Repository: jonobr1/two.js
Branch: dev
Commit: c8e5f95d2405
Files: 207
Total size: 2.6 MB
Directory structure:
gitextract_pil82dhw/
├── .eslintignore
├── .eslintrc.json
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── feature_request.md
│ │ ├── output.md
│ │ └── question.md
│ └── workflows/
│ ├── codeql.yml
│ ├── lint.yml
│ └── publish.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── build/
│ ├── two.js
│ └── two.module.js
├── deploy.sh
├── extras/
│ ├── js/
│ │ ├── arc.js
│ │ └── zui.js
│ └── jsm/
│ ├── arc.js
│ ├── zui.d.ts
│ └── zui.js
├── package.json
├── src/
│ ├── anchor.d.ts
│ ├── anchor.js
│ ├── children.d.ts
│ ├── children.js
│ ├── collection.d.ts
│ ├── collection.js
│ ├── constants.d.ts
│ ├── constants.js
│ ├── effects/
│ │ ├── gradient.d.ts
│ │ ├── gradient.js
│ │ ├── image-sequence.d.ts
│ │ ├── image-sequence.js
│ │ ├── image.d.ts
│ │ ├── image.js
│ │ ├── linear-gradient.d.ts
│ │ ├── linear-gradient.js
│ │ ├── radial-gradient.d.ts
│ │ ├── radial-gradient.js
│ │ ├── sprite.d.ts
│ │ ├── sprite.js
│ │ ├── stop.d.ts
│ │ ├── stop.js
│ │ ├── texture.d.ts
│ │ └── texture.js
│ ├── element.d.ts
│ ├── element.js
│ ├── events.d.ts
│ ├── events.js
│ ├── group.d.ts
│ ├── group.js
│ ├── matrix.d.ts
│ ├── matrix.js
│ ├── path.d.ts
│ ├── path.js
│ ├── registry.d.ts
│ ├── registry.js
│ ├── renderers/
│ │ ├── canvas.d.ts
│ │ ├── canvas.js
│ │ ├── svg.d.ts
│ │ ├── svg.js
│ │ ├── webgl.d.ts
│ │ └── webgl.js
│ ├── shape.d.ts
│ ├── shape.js
│ ├── shapes/
│ │ ├── arc-segment.d.ts
│ │ ├── arc-segment.js
│ │ ├── circle.d.ts
│ │ ├── circle.js
│ │ ├── ellipse.d.ts
│ │ ├── ellipse.js
│ │ ├── line.d.ts
│ │ ├── line.js
│ │ ├── points.d.ts
│ │ ├── points.js
│ │ ├── polygon.d.ts
│ │ ├── polygon.js
│ │ ├── rectangle.d.ts
│ │ ├── rectangle.js
│ │ ├── rounded-rectangle.d.ts
│ │ ├── rounded-rectangle.js
│ │ ├── star.d.ts
│ │ └── star.js
│ ├── text.d.ts
│ ├── text.js
│ ├── two.d.ts
│ ├── two.js
│ ├── utils/
│ │ ├── canvas-polyfill.d.ts
│ │ ├── canvas-polyfill.js
│ │ ├── curves.d.ts
│ │ ├── curves.js
│ │ ├── device-pixel-ratio.d.ts
│ │ ├── device-pixel-ratio.js
│ │ ├── dom.d.ts
│ │ ├── dom.js
│ │ ├── error.d.ts
│ │ ├── error.js
│ │ ├── hit-test.js
│ │ ├── interpret-svg.d.ts
│ │ ├── interpret-svg.js
│ │ ├── math.d.ts
│ │ ├── math.js
│ │ ├── path-commands.d.ts
│ │ ├── path-commands.js
│ │ ├── path.js
│ │ ├── root.d.ts
│ │ ├── root.js
│ │ ├── shaders.d.ts
│ │ ├── shaders.js
│ │ ├── shape.d.ts
│ │ ├── shape.js
│ │ ├── underscore.d.ts
│ │ ├── underscore.js
│ │ ├── xhr.d.ts
│ │ └── xhr.js
│ ├── vector.d.ts
│ └── vector.js
├── tests/
│ ├── index.html
│ ├── noWebGL.html
│ ├── src/
│ │ └── utils.js
│ ├── suite/
│ │ ├── bounding-box.js
│ │ ├── canvas.js
│ │ ├── core.js
│ │ ├── dispose.js
│ │ ├── hit-test.js
│ │ ├── release.js
│ │ ├── shapes.js
│ │ ├── svg-interpreter.js
│ │ ├── svg.js
│ │ └── webgl.js
│ └── typescript/
│ ├── index.js
│ ├── index.ts
│ └── package.json
├── utils/
│ ├── INSTRUCTIONS.md
│ ├── build.js
│ ├── docs.template
│ ├── document.js
│ ├── file-sizes.json
│ └── source-files.js
└── wiki/
├── .vuepress/
│ ├── components/
│ │ ├── carbon-ads.vue
│ │ ├── custom-button.vue
│ │ ├── example-card.vue
│ │ ├── inline-editor.vue
│ │ ├── redirect-page.vue
│ │ └── version-link.vue
│ ├── config.js
│ ├── enhanceApp.js
│ ├── plugins/
│ │ └── search/
│ │ ├── SearchBox.vue
│ │ └── match-query.js
│ ├── styles/
│ │ ├── index.styl
│ │ └── palette.styl
│ └── theme/
│ ├── components/
│ │ ├── Navbar.vue
│ │ └── Sidebar.vue
│ ├── index.js
│ └── layouts/
│ └── NotFound.vue
├── README.md
├── change-log/
│ └── README.md
├── changelog/
│ └── README.md
├── docs/
│ ├── README.md
│ ├── anchor/
│ │ └── README.md
│ ├── children/
│ │ └── README.md
│ ├── collection/
│ │ └── README.md
│ ├── effects/
│ │ ├── gradient/
│ │ │ └── README.md
│ │ ├── image/
│ │ │ └── README.md
│ │ ├── image-sequence/
│ │ │ └── README.md
│ │ ├── linear-gradient/
│ │ │ └── README.md
│ │ ├── radial-gradient/
│ │ │ └── README.md
│ │ ├── sprite/
│ │ │ └── README.md
│ │ ├── stop/
│ │ │ └── README.md
│ │ └── texture/
│ │ └── README.md
│ ├── element/
│ │ └── README.md
│ ├── events/
│ │ └── README.md
│ ├── extras/
│ │ ├── arc/
│ │ │ └── README.md
│ │ └── zui/
│ │ └── README.md
│ ├── group/
│ │ └── README.md
│ ├── matrix/
│ │ └── README.md
│ ├── path/
│ │ └── README.md
│ ├── registry/
│ │ └── README.md
│ ├── renderers/
│ │ ├── canvas/
│ │ │ └── README.md
│ │ ├── svg/
│ │ │ └── README.md
│ │ └── webgl/
│ │ └── README.md
│ ├── shape/
│ │ └── README.md
│ ├── shapes/
│ │ ├── arc-segment/
│ │ │ └── README.md
│ │ ├── circle/
│ │ │ └── README.md
│ │ ├── ellipse/
│ │ │ └── README.md
│ │ ├── line/
│ │ │ └── README.md
│ │ ├── points/
│ │ │ └── README.md
│ │ ├── polygon/
│ │ │ └── README.md
│ │ ├── rectangle/
│ │ │ └── README.md
│ │ ├── rounded-rectangle/
│ │ │ └── README.md
│ │ └── star/
│ │ └── README.md
│ ├── text/
│ │ └── README.md
│ ├── two/
│ │ └── README.md
│ └── vector/
│ └── README.md
├── donate/
│ └── README.md
├── examples/
│ └── README.md
├── incident-response-plan/
│ └── README.md
├── privacy/
│ └── README.md
└── security/
└── README.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
/**/*.d.ts
================================================
FILE: .eslintrc.json
================================================
{
"parser": "babel-eslint",
"extends": "eslint:recommended",
"env": {
"browser": true,
"commonjs": true,
"amd": true
},
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2015
},
"rules": {
"semi": ["error", "always"],
"no-unused-vars": ["error", {"args": "none"}]
},
"overrides": [
{
"files": ["utils/**"],
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 8
}
},
{
"files": ["tests/**", "extras/js/**"],
"globals": {
"QUnit": "writable",
"resemble": "writable",
"Two": "writable",
"_": "writable"
},
"rules": {
"no-redeclare": "off"
}
}
],
"ignorePatterns": ["build/", "utils/start-comment.js", "utils/end-comment.js", "utils/exports.js", "junk/"]
}
================================================
FILE: .gitattributes
================================================
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.gif binary
*.png binary
*.jpg binary
*.jpeg binary
*.mp4 binary
*.webm binary
*.eot binary
*.woff binary
*.woff2 binary
*.ttf binary
*.mp3 binary
*.ogg binary
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [jonobr1] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
custom: # https://two.js.org/donate/
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help improve Two.js
title: "[Bug]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please select one):**
- [ ] Code executes in browser (e.g: using script tag to load library)
- [ ] Packaged software (e.g: ES6 imports, react, angular, vue.js)
- [ ] Running headless (usually Node.js)
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea or enhancement to Two.js
title: "[Enhancement]"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/ISSUE_TEMPLATE/output.md
================================================
---
name: Output
about: Share what you have made with Two.js
title: "[Output]"
labels: output
assignees: ''
---
**What did you make with Two.js?**
A clear and concise description of what you made and how Two.js was a part of it.
**Can we showcase this on [the Two.js site](https://two.js.org/)?**
- [ ] Yes
- [ ] No
**To be included for the Two.js site:**
1. Project name
2. We need an animated gif or still image at at least 512px wide. Also, this image needs to be hosted where we can input the URL.
3. Tags: particular features of Two.js you used? Or any other defining aspects to your project that would make it easy for people to search for on the site?
**Feedback**
Anything you'd like to share with the Two.js team? Pros, cons, suggestions, praise?
**Thank you for sharing!**
================================================
FILE: .github/ISSUE_TEMPLATE/question.md
================================================
---
name: Question
about: Ask a question about Two.js
title: "[Question]"
labels: question
assignees: ''
---
**Describe your question**
**Your code (either pasted here, or a link to a hosted example)**
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please select one):**
- [ ] Code executes in browser (e.g: using script tag to load library)
- [ ] Packaged software (e.g: ES6 imports, react, angular, vue.js)
- [ ] Running headless (usually Node.js)
---
If applicable:
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/workflows/codeql.yml
================================================
name: "CodeQL"
on:
push:
branches: [ "dev" ]
pull_request:
branches: [ "dev" ]
schedule:
- cron: "22 9 * * 6"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ javascript ]
steps:
# checkout@v5
- name: Checkout repository
uses: jonobr1/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
# setup-node@v5
- name: Set up Node.js
uses: jonobr1/setup-node@a0853c24544627f65ddf259abe73b1d18a591444
with:
node-version: '20' # Use latest LTS
# cache@v4
- name: Cache node modules
uses: jonobr1/cache@0400d5f644dc74513175e3cd8d07132dd4860809
with:
path: |
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
run: npm ci
# codeql-action/init@v3
- name: Initialize CodeQL
uses: jonobr1/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3
with:
languages: ${{ matrix.language }}
queries: security-and-quality
# codeql-action/autobuild@v3
- name: Autobuild
uses: jonobr1/codeql-action/autobuild@192325c86100d080feab897ff886c34abd4c83a3
# codeql-action/analyze@v3
- name: Perform CodeQL Analysis
uses: jonobr1/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3
with:
category: "/language:${{ matrix.language }}"
================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
# checkout@v5
- uses: jonobr1/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Install modules
run: npm install
- name: Run ESLint
run: npm run lint
================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish Package
on:
push:
tags:
- 'v*'
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
# checkout@v6
- uses: jonobr1/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
# setup-node@v6
- uses: jonobr1/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
# Ensure npm 11.5.1 or later is installed
- name: Update npm
run: npm install -g npm@latest
- run: npm ci
- run: npm run build --if-present
# - run: npm test
- run: npm publish
================================================
FILE: .gitignore
================================================
node_modules
*.DS_Store
junk/
*.log
docs.json
dist
.idea
.codacy
tests/typescript/package-lock.json
#Ignore vscode AI rules
.github/instructions/codacy.instructions.md
================================================
FILE: .npmrc
================================================
package-lock = false
================================================
FILE: .nvmrc
================================================
v14.7.0
================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Two.js is a renderer-agnostic 2D drawing API for modern browsers. It provides a unified interface for creating graphics across multiple rendering contexts: WebGL, Canvas2D, and SVG.
## Core Architecture
### Main Entry Point
- `src/two.js` - Main Two.js class and entry point that imports all modules
- The Two class extends Events and provides factory methods for creating shapes
### Rendering System
- **Multi-renderer architecture**: Canvas, SVG, and WebGL renderers in `src/renderers/`
- **Scene graph**: Hierarchical structure using Groups and Elements
- **Automatic renderer selection**: Based on domElement type or explicit type specification
### Core Classes
- `Element` - Base class for all drawable objects
- `Shape` - Extended Element with transformation and styling
- `Group` - Container for organizing and transforming multiple objects
- `Path` - Complex shapes defined by anchor points and curves
- `Vector` - 2D vector mathematics
- `Matrix` - 2D transformation matrices
- `Anchor` - Control points for paths with B�zier curve handles
### Shape Library
Located in `src/shapes/`:
- Basic shapes: Rectangle, Circle, Ellipse, Line, Star, Polygon
- Complex shapes: ArcSegment, RoundedRectangle, Points
- All shapes inherit from Path or Shape classes
### Effects System
Located in `src/effects/`:
- Gradients: LinearGradient, RadialGradient with Stop objects
- Images: Texture, Sprite, ImageSequence for bitmap rendering
- All effects can be applied as fill or stroke to shapes
## Build System
### Commands
- `npm run build` - Build all versions (UMD, ESM, minified) using esbuild
- `npm run dev` - Development server with esbuild on port 8080
- `npm run lint` - ESLint with auto-fix
- `npm run docs:generate` - Generate documentation from JSDoc comments
- `npm run docs:dev` - Local documentation server with Vuepress
- `npm run docs:build` - Build static documentation site
### Build Configuration
- Build script: `utils/build.js`
- Uses esbuild for fast bundling and minification
- Outputs: `build/two.js` (UMD), `build/two.module.js` (ESM), `build/two.min.js` (minified)
- Includes license header and module.exports compatibility
## Development Patterns
### Factory Methods
The Two class provides factory methods for creating and adding objects to the scene:
- `makeRectangle()`, `makeCircle()`, `makeText()`, etc.
- All factory methods automatically add objects to the scene
- Return the created object for further manipulation
### Event System
- All objects inherit from Events class
- Common events: update, render, resize, play, pause
- Use `bind()`, `unbind()`, `trigger()` for event handling
### Coordinate System
- Origin (0,0) at top-left by default
- Positive Y axis points down
- Transformations applied via translation, rotation, scale properties
### Memory Management
- Use `release()` method to unbind events and free memory
- Automatically handles nested objects, vertices, and effects
- Important for preventing memory leaks in long-running applications
## Testing
### Test Structure
- Tests located in `tests/` directory
- Test suites in `tests/suite/` organized by functionality
- HTML test runners: `tests/index.html`, `tests/noWebGL.html`
- TypeScript compilation tests in `tests/typescript/` with `index.ts` that imports and uses Two.js API
### Running Tests
- Manual browser testing via HTML files: `tests/index.html` and `tests/noWebGL.html`
- TypeScript compilation testing: `npx tsc --noEmit --skipLibCheck tests/typescript/index.ts` to verify types work correctly
## Key Files to Understand
- `src/two.js` - Main class with factory methods and core logic
- `src/constants.js` - Global constants, types, and configuration
- `src/utils/interpret-svg.js` - SVG parsing and import functionality
- `utils/build.js` - Build system configuration
- `src/**/*.d.ts` - TypeScript definitions collocated with source files (e.g., `src/vector.d.ts` alongside `src/vector.js`)
- `src/two.d.ts` - Main TypeScript entry point that aggregates all type exports
## Dependencies
Production: None (library designed to be dependency-free)
Development: esbuild, ESLint, TypeScript, Vuepress for documentation
## Browser Compatibility
Designed for modern browsers with ES6+ support. Uses feature detection for renderer capabilities.
## Development Workflow
- Always run `npm run build && npm run lint` before committing
- Test changes in `tests/index.html` for visual verification
- Use `npm run dev` for development server on port 8080
- Check TypeScript types with `npm run types`
- Test across all three renderers (Canvas, SVG, WebGL) for compatibility
## Code Style and Conventions
- Use ES6+ features consistently
- Prefer `const` over `let` where possible
- Factory methods should always return the created object
- All classes should extend appropriate base classes (Element, Shape, etc.)
- Use JSDoc comments for public API methods
- Use 2-space indentation for JavaScript files
- Place new components in appropriate src/ subdirectories
- Application runs lots of functions on requestAnimationFrame (or per animation frame) so:
- Reduce the amount of objects and functions created within methods
- Prefer caching variables to the module scope
- Do not use function based iterators (prefer native for loops, etc.)
## Architecture Patterns
- All shapes inherit from Path or Shape classes
- Use factory methods (makeRectangle, makeCircle) instead of direct constructors
- Effects (gradients, textures) are applied via fill/stroke properties
- Memory management: always call release() for complex objects
- Event binding: use bind/unbind pattern, avoid anonymous functions
- Factory methods automatically add objects to the scene
## Common Issues and Solutions
- When adding new shapes, ensure they extend the correct base class
- WebGL renderer has different capabilities than Canvas/SVG
- Always test across all three renderers for compatibility
- SVG imports may need manual matrix calculations
- Memory leaks: unbind events in cleanup using release() method
- Coordinate system: origin (0,0) at top-left, positive Y axis points down
## Testing Guidelines
- Open `tests/index.html` in browser for manual testing
- Test new features across Canvas, SVG, and WebGL renderers
- Check `tests/noWebGL.html` for fallback scenarios
- TypeScript compilation tests: Run `npx tsc --noEmit --skipLibCheck tests/typescript/index.ts` to verify TypeScript definitions work correctly
- Manual browser testing required - no automated test runner
## File Organization Rules
- New shapes go in `src/shapes/` and follow existing naming pattern
- Effects belong in `src/effects/`
- Utilities in `src/utils/` should be pure functions
- Export new classes in `src/two.js` main file
- **TypeScript definitions**: Create a `.d.ts` file alongside each source file (e.g., `src/vector.d.ts` next to `src/vector.js`)
- Each `.d.ts` file contains a `declare module 'two.js/src/...'` block matching the module path
- Import statements go at the END of the module declaration
- Main type exports are aggregated in `src/two.d.ts`
- Renderers are in `src/renderers/` - modify with caution
## Performance Guidelines
- Minimize object creation in animation loops
- Use object pooling for frequently created/destroyed objects
- Batch DOM updates when possible
- Prefer transform operations over position updates
- Use release() method to prevent memory leaks in long-running applications
## Integration Patterns
- Node.js: Requires canvas polyfill for headless rendering
- TypeScript: Import specific modules for tree-shaking
- Bundlers: ESM build recommended for modern bundlers
- Browser: UMD build for direct script inclusion
### Nota Bene
- All visual tests run in the browser via HTML files
- TypeScript tests verify that the type definitions work correctly by compiling sample code – this is work in progress
- Manual testing approach - no automated test runners or CI integration
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at inquiries+two.js@jono.fyi. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: CONTRIBUTING.md
================================================
## How to contribute to Two.js
#### **Do you have a question?**
* All questions answered by Two.js maintainers are in the [Issues](https://github.com/jonobr1/two.js/issues?q=label%3Aquestion) section of the project with the label question. Ensure the question was not already asked by checking there first.
* If you're unable to find a question that has been answered, [create one](https://github.com/jonobr1/two.js/issues/new?assignees=&labels=question&template=question.md&title=%5BQuestion%5D). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** in situ or on a third party site like [CodePen](http://codepen.io), [jsfiddle](http://jsfiddle.com), or [glitch](http://glitch.com). This helps us to better help you.
#### **Did you find a bug?**
* **Ensure the bug was not already reported** by searching on the project under [Issues](https://github.com/jonobr1/two.js/issues).
* If you're unable to find an open issue, [open a new one](https://github.com/jonobr1/two.js/issues/new?assignees=&labels=bug&template=bug_report.md&title=%5BBug%5D). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** in situ or on a third party site like [CodePen](http://codepen.io), [jsfiddle](http://jsfiddle.com), or [glitch](http://glitch.com) demonstrating the issue.
#### **Did you write a patch that fixes a bug, add a new feature, or change an existing one?**
* Open a new GitHub pull request with the patch.
* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
#### **Do you want to contribute to the Two.js documentation?**
* This is still in its infancy and if you're interested to help out, please send an email to [inquires@jono.fyi](mailto:inquiries@jono.fyi) with the subject "Two.js Documentation". In the body of your email please describe why or how you'd like to help.
Two.js is a volunteer effort, so we apologize in advance for any delays.
Thanks! For both taking the time to read this and contributing.
Much :heart: from the Two.js Team
---
This document is adapted from the [Ruby on Rails](https://github.com/rails/rails/blob/main/CONTRIBUTING.md#how-to-contribute-to-ruby-on-rails) project.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2012 - 2025 @jonobr1 / http://jono.fyi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Two.js
[![NPM Package][npm]][npm-url]
[![Build Size][build-size]][build-size-url]
[![NPM Downloads][npm-downloads]][npmtrends-url]
A two-dimensional drawing api meant for modern browsers. It is renderer agnostic enabling the same api to render in multiple contexts: webgl, canvas2d, and svg.
[Home](http://two.js.org/) • [Releases](https://github.com/jonobr1/two.js/releases) • [Examples](http://two.js.org/examples/) • [Documentation](https://two.js.org/docs/two/) • [Changelog](https://github.com/jonobr1/two.js/tree/dev/wiki/changelog) • [Help](https://github.com/jonobr1/two.js/issues/new/choose)
## Usage
Download the latest [minified library](https://raw.github.com/jonobr1/two.js/dev/build/two.min.js) and include it in your html.
```html
```
It can also be installed via [npm](https://www.npmjs.com/package/two.js), Node Package Manager:
```js
npm install --save two.js
```
Alternatively see [how to build the library yourself](https://github.com/jonobr1/two.js#custom-build).
Here is boilerplate html in order to draw a spinning rectangle in two.js:
```html
```
## Custom Build
Two.js uses [nodejs](http://nodejs.org/) in order to build source files. You'll first want to install that. Once installed open up a terminal and head to the repository folder:
```
cd ~/path-to-repo/two.js
npm install
```
This will give you a number of libraries that the development of Two.js relies on. If for instance you only use the `SVGRenderer` then you can really cut down on the file size by excluding the other renderers. To do this, modify `/utils/build.js` to only add the files you'd like. Then run:
```
node ./utils/build
```
And the resulting `/build/two.js` and `/build/two.min.js` will be updated to your specification.
---
### Using ES6 Imports
As of version `v0.7.5+` Two.js is compatible with EcmaScript 6 imports. This is typically employed in contemporary frameworks like [React](https://reactjs.org/) and [Angular](https://angularjs.org/) as well as bundling libraries like [webpack](https://webpack.js.org/), [esbuild](https://esbuild.github.io/), and [gulp](https://gulpjs.com/). This adaptation of the boilerplate can be found on [CodeSandbox](https://codesandbox.io/s/beautiful-wilbur-ygxbc?file=/src/App.js:0-664):
```jsx
import React, { useEffect, useRef } from "react";
import Two from "two.js";
export default function App() {
var domElement = useRef();
useEffect(setup, []);
function setup() {
var two = new Two({
fullscreen: true,
autostart: true
}).appendTo(domElement.current);
var rect = two.makeRectangle(two.width / 2, two.height / 2, 50, 50);
two.bind("update", update);
return unmount;
function unmount() {
two.unbind("update");
two.pause();
domElement.current.removeChild(two.renderer.domElement);
}
function update() {
rect.rotation += 0.001;
}
}
return ;
}
```
In addition to importing, the published packages of Two.js include the specific modules. So, if necessary you can import specific modules from the source code and bundle / minify for yourself like so:
```javascript
import { Vector } from 'two.js/src/vector.js';
// In TypeScript environments leave out the ".js"
// when importing modules directly. e.g:
import { Vector } from 'two.js/src/vector';
```
_While useful, the main import of the `Two` namespace imports all modules. So, there isn't yet proper tree shaking implemented for the library, though it's on the roadmap._
### Running in Headless Environments
As of version `v0.7.x` Two.js can also run in a headless environment, namely running on the server with the help of a library called [Node Canvas](https://github.com/Automattic/node-canvas). We don't add Node Canvas to dependencies of Two.js because it's _not necessary_ to run it in the browser. However, it has all the hooks set up to run in a cloud environment. To get started follow the installation instructions on Automattic's [readme](https://github.com/Automattic/node-canvas#installation). After you've done that run:
```
npm install canvas
npm install two.js
```
Now in a JavaScript file set up your Two.js scenegraph and save out frames whenever you need to:
```javascript
var { createCanvas, Image } = require('canvas');
var Two = require('two.js')
var fs = require('fs');
var path = require('path');
var width = 800;
var height = 600;
var canvas = createCanvas(width, height);
Two.Utils.polyfill(canvas, Image);
var time = Date.now();
var two = new Two({
width: width,
height: height,
domElement: canvas
});
var rect = two.makeRectangle(width / 2, height / 2, 50, 50);
rect.fill = 'rgb(255, 100, 100)';
rect.noStroke();
two.render();
var settings = { compressionLevel: 3, filters: canvas.PNG_FILTER_NONE };
fs.writeFileSync(path.resolve(__dirname, './images/rectangle.png'), canvas.toBuffer('image/png', settings));
console.log('Finished rendering. Time took: ', Date.now() - time);
process.exit();
```
## Build Documentation
The [Two.js](http://two.js.org/) website is bundled with this repository. Relying on [Vuepress](https://vuepress.vuejs.org/) the repository generates a website based on numerous `README.md` files housed in the `wiki` directory. Use the following the node commands as follows:
```bash
npm run docs:generate // Generate README.md files for documentation from source code comments
npm run docs:dev // Creates a local server to generate all documentation
npm run docs:build // Builds out static site and associated files to wiki/.vuepress/dist
```
N.B: Vuepress is a legacy library and as such these commands rely on an older version of Node. Run `nvm use` if you get errors. If you don't use [Node Version Manager](https://github.com/nvm-sh/nvm) then see `.nvmrc` to install the correct version of node on your local machine.
## Change Log
Two.js has been in operation since 2012. For a full list of changes from its first alpha version built with [Three.js](http://threejs.org/) to the most up-to-date tweaks. Check out the wiki [here](./wiki/changelog).
---
#### And a big thank you to our sponsors who include:
[Epilogue Press](https://github.com/epiloguepress)
[npm]: https://img.shields.io/npm/v/two.js
[npm-url]: https://www.npmjs.com/package/two.js
[build-size]: https://badgen.net/bundlephobia/minzip/two.js
[build-size-url]: https://bundlephobia.com/result?p=two.js
[npm-downloads]: https://img.shields.io/npm/dt/two.js
[npmtrends-url]: https://www.npmtrends.com/two.js
================================================
FILE: SECURITY.md
================================================
# Security Policy
If you have discovered a security vulnerability in this project, please report it privately. **Do not disclose it as a public issue.** This gives us time to work with you to fix the issue before public exposure, reducing the chance that the exploit will be used before a patch is released.
**You may submit the report in the following ways:**
- Github users can privately report security advisories directly [here](https://github.com/jonobr1/two.js/security/advisories/new)
- Send an email to [inquiries+two.js@jono.fyi](mailto:inquiries+two.js@jono.fyi).
**Please provide the following information in your report:**
- The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting)
- Full paths of source file(s) related to the manifestation of the issue
- The location of the affected source code (tag/branch/commit or direct URL)
- Any special configuration required to reproduce the issue
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue, including how an attacker might exploit the issue
This project is maintained by volunteers on a reasonable-effort basis. As such, we ask that you give us 90 days to work on a fix before public exposure.
---
_Two.js conforms to this [Incident Response Plan](https://two.js.org/incident-response-plan) in moments of security risks._
================================================
FILE: build/two.js
================================================
/*
MIT License
Copyright (c) 2012 - 2025 @jonobr1 / http://jono.fyi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
var Two = (() => {
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod2) => __copyProps(__defProp({}, "__esModule", { value: true }), mod2);
// src/two.js
var two_exports = {};
__export(two_exports, {
default: () => Two
});
// src/utils/canvas-polyfill.js
var CanvasPolyfill = {
/**
* @param {Image}
*/
Image: null,
/**
* @param {Boolean}
*/
isHeadless: false,
/**
*
* @param {canvas} elem - An element to spoof as a ``.
* @param {String} [name] - An optional tag and node name to spoof. Defaults to `'canvas'`.
* @returns {canvas} - The same `elem` passed in the first argument with updated attributes needed to be used by Two.js.
* @description Adds attributes invoked by Two.js in order to execute and run correctly. This is used by headless environments.
*/
shim: function(elem, name) {
elem.tagName = elem.nodeName = name || "canvas";
elem.nodeType = 1;
elem.getAttribute = function(prop) {
return this[prop];
};
elem.setAttribute = function(prop, val) {
this[prop] = val;
return this;
};
return elem;
},
/**
* @name Two.Utils.polyfill
* @function
* @param {canvas} canvas - The instanced `Canvas` object provided by `node-canvas`.
* @param {Image} [Image] - The prototypical `Image` object provided by `node-canvas`. This is only necessary to pass if you're going to load bitmap imagery.
* @returns {canvas} Returns the instanced canvas object you passed from with additional attributes needed for Two.js.
* @description Convenience method for defining all the dependencies from the npm package `node-canvas`. See [node-canvas](https://github.com/Automattic/node-canvas) for additional information on setting up HTML5 `` drawing in a node.js environment.
*/
polyfill: function(canvas3, Image3) {
CanvasPolyfill.shim(canvas3);
if (typeof Image3 !== "undefined") {
CanvasPolyfill.Image = Image3;
}
CanvasPolyfill.isHeadless = true;
return canvas3;
}
};
// src/utils/curves.js
var curves_exports = {};
__export(curves_exports, {
Curve: () => Curve,
getAnchorsFromArcData: () => getAnchorsFromArcData,
getComponentOnCubicBezier: () => getComponentOnCubicBezier,
getControlPoints: () => getControlPoints,
getCurveBoundingBox: () => getCurveBoundingBox,
getCurveFromPoints: () => getCurveFromPoints,
getCurveLength: () => getCurveLength,
getReflection: () => getReflection,
integrate: () => integrate,
subdivide: () => subdivide
});
// src/utils/math.js
var math_exports = {};
__export(math_exports, {
HALF_PI: () => HALF_PI,
NumArray: () => NumArray,
TWO_PI: () => TWO_PI,
decomposeMatrix: () => decomposeMatrix,
getComputedMatrix: () => getComputedMatrix,
getEffectiveStrokeWidth: () => getEffectiveStrokeWidth,
getPoT: () => getPoT,
lerp: () => lerp,
mod: () => mod,
setMatrix: () => setMatrix,
toFixed: () => toFixed
});
// src/utils/root.js
var root;
if (typeof window !== "undefined") {
root = window;
} else if (typeof global !== "undefined") {
root = global;
} else if (typeof self !== "undefined") {
root = self;
}
// src/utils/math.js
var Matrix;
var TWO_PI = Math.PI * 2;
var HALF_PI = Math.PI * 0.5;
function decomposeMatrix(matrix, b, c, d, e, f) {
let a;
if (arguments.length <= 1) {
a = matrix.a;
b = matrix.b;
c = matrix.c;
d = matrix.d;
e = matrix.e;
f = matrix.f;
} else {
a = matrix;
}
return {
translateX: e,
translateY: f,
scaleX: Math.sqrt(a * a + b * b),
scaleY: Math.sqrt(c * c + d * d),
rotation: 180 * Math.atan2(b, a) / Math.PI
};
}
function setMatrix(matrix) {
Matrix = matrix;
}
function getComputedMatrix(object, matrix) {
matrix = matrix && matrix.identity() || new Matrix();
let parent = object;
const matrices = [];
while (parent && parent._matrix) {
matrices.push(parent._matrix);
parent = parent.parent;
}
matrices.reverse();
for (let i = 0; i < matrices.length; i++) {
const m = matrices[i];
const e = m.elements;
matrix.multiply(
e[0],
e[1],
e[2],
e[3],
e[4],
e[5],
e[6],
e[7],
e[8]
);
}
return matrix;
}
function lerp(a, b, t) {
return t * (b - a) + a;
}
var pots = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096];
function getPoT(value) {
let i = 0;
while (pots[i] && pots[i] < value) {
i++;
}
return pots[i];
}
function mod(v, l) {
while (v < 0) {
v += l;
}
return v % l;
}
var NumArray = root.Float32Array || Array;
var floor = Math.floor;
function toFixed(v) {
return floor(v * 1e6) / 1e6;
}
function getEffectiveStrokeWidth(object, worldMatrix) {
const linewidth = object._linewidth;
if (object.strokeAttenuation) {
return linewidth;
}
if (!worldMatrix) {
worldMatrix = object.worldMatrix || getComputedMatrix(object);
}
const decomposed = decomposeMatrix(
worldMatrix.elements[0],
worldMatrix.elements[3],
worldMatrix.elements[1],
worldMatrix.elements[4],
worldMatrix.elements[2],
worldMatrix.elements[5]
);
const scale = Math.max(Math.abs(decomposed.scaleX), Math.abs(decomposed.scaleY));
return scale > 0 ? linewidth / scale : linewidth;
}
// src/utils/path-commands.js
var Commands = {
move: "M",
line: "L",
curve: "C",
arc: "A",
close: "Z"
};
// src/events.js
var Events = class {
_events = {};
_bound = false;
constructor() {
}
/**
* @name Two.Events#addEventListener
* @function
* @param {String} [name] - The name of the event to bind a function to.
* @param {Function} [handler] - The function to be invoked when the event is dispatched.
* @description Call to add a listener to a specific event name.
*/
addEventListener(name, handler) {
const list = this._events[name] || (this._events[name] = []);
list.push(handler);
this._bound = true;
return this;
}
/**
* @name Two.Events#on
* @function
* @description Alias for {@link Two.Events#addEventListener}.
*/
on() {
return this.addEventListener.apply(this, arguments);
}
/**
* @name Two.Events#bind
* @function
* @description Alias for {@link Two.Events#addEventListener}.
*/
bind() {
return this.addEventListener.apply(this, arguments);
}
/**
* @name Two.Events#removeEventListener
* @function
* @param {String} [name] - The name of the event intended to be removed.
* @param {Function} [handler] - The handler intended to be removed.
* @description Call to remove listeners from a specific event. If only `name` is passed then all the handlers attached to that `name` will be removed. If no arguments are passed then all handlers for every event on the obejct are removed.
*/
removeEventListener(name, handler) {
if (!this._events) {
return this;
}
if (!name && !handler) {
this._events = {};
this._bound = false;
return this;
}
const names = name ? [name] : Object.keys(this._events);
for (let i = 0, l = names.length; i < l; i++) {
name = names[i];
let list = this._events[name];
if (list) {
let events = [];
if (handler) {
for (let j = 0, k = list.length; j < k; j++) {
let e = list[j];
e = e.handler ? e.handler : e;
if (handler !== e) {
events.push(e);
}
}
}
this._events[name] = events;
}
}
return this;
}
/**
* @name Two.Events#off
* @function
* @description Alias for {@link Two.Events#removeEventListener}.
*/
off() {
return this.removeEventListener.apply(this, arguments);
}
/**
* @name Two.Events#unbind
* @function
* @description Alias for {@link Two.Events#removeEventListener}.
*/
unbind() {
return this.removeEventListener.apply(this, arguments);
}
/**
* @name Two.Events#dispatchEvent
* @function
* @param {String} name - The name of the event to dispatch.
* @param args - Anything can be passed after the name and those will be passed on to handlers attached to the event in the order they are passed.
* @description Call to trigger a custom event. Any additional arguments passed after the name will be passed along to the attached handlers.
*/
dispatchEvent(name) {
if (!this._events) {
return this;
}
const args = Array.prototype.slice.call(arguments, 1);
const events = this._events[name];
if (events) {
for (let i = 0; i < events.length; i++) {
events[i].call(this, ...args);
}
}
return this;
}
trigger() {
return this.dispatchEvent.apply(this, arguments);
}
listen(obj, name, handler) {
const scope = this;
if (obj) {
e.obj = obj;
e.name = name;
e.handler = handler;
obj.on(name, e);
}
function e() {
handler.apply(scope, arguments);
}
return scope;
}
ignore(obj, name, handler) {
obj.off(name, handler);
return this;
}
/**
* @name Two.Events.Types
* @property {Object} - Object of different types of Two.js specific events.
*/
static Types = {
play: "play",
pause: "pause",
update: "update",
render: "render",
resize: "resize",
change: "change",
remove: "remove",
insert: "insert",
order: "order",
load: "load"
};
static Methods = [
"addEventListener",
"on",
"removeEventListener",
"off",
"unbind",
"dispatchEvent",
"trigger",
"listen",
"ignore"
];
};
// src/vector.js
var proto = {
x: {
enumerable: true,
get: function() {
return this._x;
},
set: function(v) {
if (this._x !== v) {
this._x = v;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
},
y: {
enumerable: true,
get: function() {
return this._y;
},
set: function(v) {
if (this._y !== v) {
this._y = v;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
}
};
var Vector = class _Vector extends Events {
/**
* @name Two.Vector#_x
* @private
*/
_x = 0;
/**
* @name Two.Vector#_y
* @private
*/
_y = 0;
constructor(x = 0, y = 0) {
super();
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this.x = x;
this.y = y;
}
/**
* @name Two.Vector.zero
* @readonly
* @property {Two.Vector} - Handy reference to a vector with component values 0, 0 at all times.
*/
static zero = new _Vector();
/**
* @name Two.Vector.left
* @readonly
* @property {Two.Vector} - Handy reference to a vector with component values -1, 0 at all times.
*/
static left = new _Vector(-1, 0);
/**
* @name Two.Vector.right
* @readonly
* @property {Two.Vector} - Handy reference to a vector with component values 1, 0 at all times.
*/
static right = new _Vector(1, 0);
/**
* @name Two.Vector.up
* @readonly
* @property {Two.Vector} - Handy reference to a vector with component values 0, -1 at all times.
*/
static up = new _Vector(0, -1);
/**
* @name Two.Vector.down
* @readonly
* @property {Two.Vector} - Handy reference to a vector with component values 0, 1 at all times.
*/
static down = new _Vector(0, 1);
/**
* @name Two.Vector.add
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Two.Vector}
* @description Add two vectors together.
*/
static add(v1, v2) {
return new _Vector(v1.x + v2.x, v1.y + v2.y);
}
/**
* @name Two.Vector.sub
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Two.Vector}
* @description Subtract two vectors: `v2` from `v1`.
*/
static sub(v1, v2) {
return new _Vector(v1.x - v2.x, v1.y - v2.y);
}
/**
* @name Two.Vector.subtract
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
static subtract(v1, v2) {
return _Vector.sub(v1, v2);
}
/**
* @name Two.Vector.ratioBetween
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Number} The ratio betwen two points `v1` and `v2`.
*/
static ratioBetween(v1, v2) {
return (v1.x * v2.x + v1.y * v2.y) / (v1.length() * v2.length());
}
/**
* @name Two.Vector.angleBetween
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Number} The angle between points `v1` and `v2`.
*/
static angleBetween(v1, v2) {
if (arguments.length >= 4) {
const dx2 = arguments[0] - arguments[2];
const dy2 = arguments[1] - arguments[3];
return Math.atan2(dy2, dx2);
}
const dx = v1.x - v2.x;
const dy = v1.y - v2.y;
return Math.atan2(dy, dx);
}
/**
* @name Two.Vector.distanceBetween
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Number} The distance between points `v1` and `v2`. Distance is always positive.
*/
static distanceBetween(v1, v2) {
return Math.sqrt(_Vector.distanceBetweenSquared(v1, v2));
}
/**
* @name Two.Vector.distanceBetweenSquared
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Number} The squared distance between points `v1` and `v2`.
*/
static distanceBetweenSquared(v1, v2) {
const dx = v1.x - v2.x;
const dy = v1.y - v2.y;
return dx * dx + dy * dy;
}
//
/**
* @name Two.Vector#set
* @function
* @param {number} x - Value of `x` component
* @param {number} y - Value of `y` component
*/
set(x, y) {
this.x = x;
this.y = y;
return this;
}
/**
* @name Two.Vector#copy
* @function
* @param {Two.Vector} v - The {@link Two.Vector} to copy
* @description Copy the `x` / `y` components of another object {@link Two.Vector}.
*/
copy(v) {
this.x = v.x;
this.y = v.y;
return this;
}
/**
* @name Two.Vector#clear
* @function
* @description Set the `x` / `y` component values of the vector to zero.
*/
clear() {
this.x = 0;
this.y = 0;
return this;
}
/**
* @name Two.Vector#clone
* @function
* @description Create a new vector and copy the existing values onto the newly created instance.
* @return {Two.Vector}
*/
clone() {
return new _Vector(this.x, this.y);
}
/**
* @name Two.Vector#add
* @function
* @param {Two.Vector} v - The {@link Two.Vector} to add
* @description Add an object with `x` / `y` component values to the instance.
* @overloaded
*/
/**
* @name Two.Vector#add
* @function
* @param {Number} n - Number to add
* @description Add the **same** number to both `x` / `y` component values of the instance.
* @overloaded
*/
/**
* @name Two.Vector#add
* @function
* @param {Number} x - Number to add to `x` component
* @param {Number} y - Number to add to `y` component
* @description Add `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
add(x, y) {
if (arguments.length <= 0) {
return this;
} else if (arguments.length <= 1) {
if (typeof x === "number") {
this.x += x;
this.y += x;
} else if (x && typeof x.x === "number" && typeof x.y === "number") {
this.x += x.x;
this.y += x.y;
}
} else {
this.x += x;
this.y += y;
}
return this;
}
/**
* @name Two.Vector#addSelf
* @function
* @description Alias for {@link Two.Vector.add}.
*/
addSelf(v) {
return this.add.apply(this, arguments);
}
/**
* @name Two.Vector#sub
* @function
* @param {Two.Vector} v - The amount as a {@link Two.Vector} to subtract
* @description Subtract an object with `x` / `y` component values to the instance.
* @overloaded
*/
/**
* @name Two.Vector#sub
* @function
* @param {Number} n - Number to subtract
* @description Subtract the **same** number to both `x` / `y` component values of the instance.
* @overloaded
*/
/**
* @name Two.Vector#sub
* @function
* @param {Number} x - Number to subtract from `x` component
* @param {Number} y - Number to subtract from `y` component
* @description Subtract `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
sub(x, y) {
if (arguments.length <= 0) {
return this;
} else if (arguments.length <= 1) {
if (typeof x === "number") {
this.x -= x;
this.y -= x;
} else if (x && typeof x.x === "number" && typeof x.y === "number") {
this.x -= x.x;
this.y -= x.y;
}
} else {
this.x -= x;
this.y -= y;
}
return this;
}
/**
* @name Two.Vector#subtract
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
subtract() {
return this.sub.apply(this, arguments);
}
/**
* @name Two.Vector#subSelf
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
subSelf(v) {
return this.sub.apply(this, arguments);
}
/**
* @name Two.Vector#subtractSelf
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
subtractSelf(v) {
return this.sub.apply(this, arguments);
}
/**
* @name Two.Vector#multiply
* @function
* @param {Two.Vector} v - The {@link Two.Vector} to multiply
* @description Multiply an object with `x` / `y` component values to the instance.
* @overloaded
*/
/**
* @name Two.Vector#multiply
* @function
* @param {Number} n - The number to multiply
* @description Multiply the **same** number to both x / y component values of the instance.
* @overloaded
*/
/**
* @name Two.Vector#multiply
* @function
* @param {Number} x - The number to multiply to `x` component
* @param {Number} y - The number to multiply to `y` component
* @description Multiply `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
multiply(x, y) {
if (arguments.length <= 0) {
return this;
} else if (arguments.length <= 1) {
if (typeof x === "number") {
this.x *= x;
this.y *= x;
} else if (x && typeof x.x === "number" && typeof x.y === "number") {
this.x *= x.x;
this.y *= x.y;
}
} else {
this.x *= x;
this.y *= y;
}
return this;
}
/**
* @name Two.Vector#multiplySelf
* @function
* @description Alias for {@link Two.Vector.multiply}.
*/
multiplySelf(v) {
return this.multiply.apply(this, arguments);
}
/**
* @name Two.Vector#multiplyScalar
* @function
* @param {Number} s - The scalar to multiply by.
* @description Mulitiply the vector by a single number. Shorthand to call {@link Two.Vector#multiply} directly.
*/
multiplyScalar(s) {
return this.multiply(s);
}
/**
* @name Two.Vector#divide
* @function
* @param {Two.Vector} v - The {@link Two.Vector} to divide
* @description Divide an object with `x` / `y` component values to the instance.
* @overloaded
*/
/**
* @name Two.Vector#divide
* @function
* @param {Number} n - The number to divide
* @description Divide the **same** number to both x / y component values of the instance.
* @overloaded
*/
/**
* @name Two.Vector#divide
* @function
* @param {Number} x - The number to divide on the `x` component
* @param {Number} y - The number to divide on the `y` component
* @description Divide `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
divide(x, y) {
if (arguments.length <= 0) {
return this;
} else if (arguments.length <= 1) {
if (typeof x === "number") {
this.x /= x;
this.y /= x;
} else if (x && typeof x.x === "number" && typeof x.y === "number") {
this.x /= x.x;
this.y /= x.y;
}
} else {
this.x /= x;
this.y /= y;
}
if (isNaN(this.x)) {
this.x = 0;
}
if (isNaN(this.y)) {
this.y = 0;
}
return this;
}
/**
* @name Two.Vector#divideSelf
* @function
* @description Alias for {@link Two.Vector.divide}.
*/
divideSelf(v) {
return this.divide.apply(this, arguments);
}
/**
* @name Two.Vector#divideScalar
* @function
* @param {Number} s - The scalar to divide by.
* @description Divide the vector by a single number. Shorthand to call {@link Two.Vector#divide} directly.
*/
divideScalar(s) {
return this.divide(s);
}
/**
* @name Two.Vector#negate
* @function
* @description Invert each component's sign value.
*/
negate() {
return this.multiply(-1);
}
/**
* @name Two.Vector#dot
* @function
* @returns {Number}
* @description Get the [dot product](https://en.wikipedia.org/wiki/Dot_product) of the vector.
*/
dot(v) {
return this.x * v.x + this.y * v.y;
}
/**
* @name Two.Vector#length
* @function
* @returns {Number}
* @description Get the length of a vector.
*/
length() {
return Math.sqrt(this.lengthSquared());
}
/**
* @name Two.Vector#lengthSquared
* @function
* @returns {Number}
* @description Get the length of the vector to the power of two. Widely used as less expensive than {@link Two.Vector#length} because it isn't square-rooting any numbers.
*/
lengthSquared() {
return this.x * this.x + this.y * this.y;
}
/**
* @name Two.Vector#normalize
* @function
* @description Normalize the vector from negative one to one.
*/
normalize() {
return this.divideScalar(this.length());
}
/**
* @name Two.Vector#distanceTo
* @function
* @returns {Number}
* @description Get the distance between two vectors.
*/
distanceTo(v) {
return Math.sqrt(this.distanceToSquared(v));
}
/**
* @name Two.Vector#distanceToSquared
* @function
* @returns {Number}
* @description Get the distance between two vectors to the power of two. Widely used as less expensive than {@link Two.Vector#distanceTo} because it isn't square-rooting any numbers.
*/
distanceToSquared(v) {
const dx = this.x - v.x;
const dy = this.y - v.y;
return dx * dx + dy * dy;
}
/**
* @name Two.Vector#setLength
* @function
* @param {Number} l - length to set vector to.
* @description Set the length of a vector.
*/
setLength(l) {
return this.normalize().multiplyScalar(l);
}
/**
* @name Two.Vector#equals
* @function
* @param {Two.Vector} v - The vector to compare against.
* @param {Number} [eps=0.0001] - An options epsilon for precision.
* @returns {Boolean}
* @description Qualify if one vector roughly equal another. With a margin of error defined by epsilon.
*/
equals(v, eps) {
eps = typeof eps === "undefined" ? 1e-4 : eps;
return this.distanceTo(v) < eps;
}
/**
* @name Two.Vector#lerp
* @function
* @param {Two.Vector} v - The destination vector to step towards.
* @param {Number} t - The zero to one value of how close the current vector gets to the destination vector.
* @description Linear interpolate one vector to another by an amount `t` defined as a zero to one number.
* @see [Matt DesLauriers](https://twitter.com/mattdesl/status/1031305279227478016) has a good thread about this.
*/
lerp(v, t) {
const x = (v.x - this.x) * t + this.x;
const y = (v.y - this.y) * t + this.y;
return this.set(x, y);
}
/**
* @name Two.Vector#isZero
* @function
* @param {Number} [eps=0.0001] - Optional precision amount to check against.
* @returns {Boolean}
* @description Check to see if vector is roughly zero, based on the `epsilon` precision value.
*/
isZero(eps) {
eps = typeof eps === "undefined" ? 1e-4 : eps;
return this.length() < eps;
}
/**
* @name Two.Vector#toString
* @function
* @returns {String}
* @description Return a comma-separated string of x, y value. Great for storing in a database.
*/
toString() {
return this.x + ", " + this.y;
}
/**
* @name Two.Vector#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the vector.
*/
toObject() {
return { x: toFixed(this.x), y: toFixed(this.y) };
}
/**
* @name Two.Vector#rotate
* @function
* @param {Number} radians - The amount to rotate the vector by in radians.
* @description Rotate a vector.
*/
rotate(radians) {
const x = this.x;
const y = this.y;
const cos7 = Math.cos(radians);
const sin7 = Math.sin(radians);
this.x = x * cos7 - y * sin7;
this.y = x * sin7 + y * cos7;
return this;
}
};
// src/anchor.js
var Anchor = class _Anchor extends Vector {
controls = {
left: new Vector(),
right: new Vector()
};
_command = Commands.move;
_relative = true;
_rx = 0;
_ry = 0;
_xAxisRotation = 0;
_largeArcFlag = 0;
_sweepFlag = 1;
constructor(x = 0, y = 0, ax = 0, ay = 0, bx = 0, by = 0, command = Commands.move) {
super(x, y);
for (let prop in proto2) {
Object.defineProperty(this, prop, proto2[prop]);
}
this.command = command;
this.relative = true;
const broadcast = _Anchor.makeBroadcast(this);
this.controls.left.set(ax, ay).addEventListener(Events.Types.change, broadcast);
this.controls.right.set(bx, by).addEventListener(Events.Types.change, broadcast);
}
static makeBroadcast(scope) {
return broadcast;
function broadcast() {
if (scope._bound) {
scope.dispatchEvent(Events.Types.change);
}
}
}
/**
* @name Two.Anchor.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Anchor} to create a new instance
* @returns {Two.Anchor}
* @description Create a new {@link Two.Anchor} from an object notation of a {@link Two.Anchor}.
* @nota-bene Works in conjunction with {@link Two.Anchor#toObject}
*/
static fromObject(obj) {
return new _Anchor().copy(obj);
}
/**
* @name Two.Anchor#copy
* @function
* @param {Two.Anchor} v - The anchor to apply values to.
* @description Copy the properties of one {@link Two.Anchor} onto another.
*/
copy(v) {
this.x = v.x;
this.y = v.y;
if (typeof v.command === "string") {
this.command = v.command;
}
if (v.controls) {
if (v.controls.left) {
this.controls.left.copy(v.controls.left);
}
if (v.controls.right) {
this.controls.right.copy(v.controls.right);
}
}
if (typeof v.relative === "boolean") {
this.relative = v.relative;
}
if (typeof v.rx === "number") {
this.rx = v.rx;
}
if (typeof v.ry === "number") {
this.ry = v.ry;
}
if (typeof v.xAxisRotation === "number") {
this.xAxisRotation = v.xAxisRotation;
}
if (typeof v.largeArcFlag === "number") {
this.largeArcFlag = v.largeArcFlag;
}
if (typeof v.sweepFlag === "number") {
this.sweepFlag = v.sweepFlag;
}
return this;
}
/**
* @name Two.Anchor#clone
* @function
* @returns {Two.Anchor}
* @description Create a new {@link Two.Anchor}, set all its values to the current instance and return it for use.
*/
clone() {
return new _Anchor().copy(this);
}
/**
* @name Two.Anchor#toObject
* @function
* @returns {Object} - An object with properties filled out to mirror {@link Two.Anchor}.
* @description Create a JSON compatible plain object of the current instance. Intended for use with storing values in a database.
* @nota-bene Works in conjunction with {@link Two.Anchor.fromObject}
*/
toObject() {
return {
x: toFixed(this.x),
y: toFixed(this.y),
command: this.command,
relative: this.relative,
controls: {
left: this.controls.left.toObject(),
right: this.controls.right.toObject()
},
rx: toFixed(this.rx),
ry: toFixed(this.ry),
xAxisRotation: toFixed(this.xAxisRotation),
largeArcFlag: toFixed(this.largeArcFlag),
sweepFlag: toFixed(this.sweepFlag)
};
}
/**
* @name Two.Anchor#toString
* @function
* @returns {String} - A String with comma-separated values reflecting the various values on the current instance.
* @description Create a string form of the current instance. Intended for use with storing values in a database. This is lighter to store than the JSON compatible {@link Two.Anchor#toObject}.
*/
toString() {
return JSON.stringify(this.toObject());
}
};
var proto2 = {
command: {
enumerable: true,
get: function() {
return this._command;
},
set: function(command) {
if (this._command !== command) {
this._command = command;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
},
relative: {
enumerable: true,
get: function() {
return this._relative;
},
set: function(relative) {
if (this._relative !== !!relative) {
this._relative = !!relative;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
},
rx: {
enumerable: true,
get: function() {
return this._rx;
},
set: function(rx) {
if (this._rx !== rx) {
this._rx = rx;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
},
ry: {
enumerable: true,
get: function() {
return this._ry;
},
set: function(ry) {
if (this._ry !== ry) {
this._ry = ry;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
},
xAxisRotation: {
enumerable: true,
get: function() {
return this._xAxisRotation;
},
set: function(xAxisRotation) {
if (this._xAxisRotation !== xAxisRotation) {
this._xAxisRotation = xAxisRotation;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
},
largeArcFlag: {
enumerable: true,
get: function() {
return this._largeArcFlag;
},
set: function(largeArcFlag) {
if (this._largeArcFlag !== largeArcFlag) {
this._largeArcFlag = largeArcFlag;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
},
sweepFlag: {
get: function() {
return this._sweepFlag;
},
set: function(sweepFlag) {
if (this._sweepFlag !== sweepFlag) {
this._sweepFlag = sweepFlag;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
}
};
// src/constants.js
var count = 0;
var Constants = {
/**
* @name Two.NextFrameId
* @property {Number}
* @description The id of the next `requestAnimationFrame` function. Used to control the (or cancel) the default behavior of Two.js animation loops.
*/
NextFrameId: null,
// Primitive
/**
* @name Two.Types
* @property {Object} - The different rendering types available in the library.
*/
Types: {
webgl: "WebGLRenderer",
svg: "SVGRenderer",
canvas: "CanvasRenderer"
},
/**
* @name Two.Version
* @property {String} - The current working version of the library.
*/
Version: "v0.8.23",
/**
* @name Two.PublishDate
* @property {String} - The automatically generated publish date in the build process to verify version release candidates.
*/
PublishDate: "2026-01-05T18:28:31.207Z",
/**
* @name Two.Identifier
* @property {String} - String prefix for all Two.js object's ids. This trickles down to SVG ids.
*/
Identifier: "two-",
/**
* @name Two.Resolution
* @property {Number} - Default amount of vertices to be used for interpreting Arcs and ArcSegments.
*/
Resolution: 12,
/**
* @name Two.AutoCalculateImportedMatrices
* @property {Boolean} - When importing SVGs through the {@link Two#interpret} and {@link Two#load}, this boolean determines whether Two.js infers and then overrides the exact transformation matrix of the reference SVG.
* @nota-bene `false` copies the exact transformation matrix values, but also sets the path's `matrix.manual = true`.
*/
AutoCalculateImportedMatrices: true,
/**
* @name Two.Instances
* @property {Two[]} - Registered list of all Two.js instances in the current session.
*/
Instances: [],
/**
* @function Two.uniqueId
* @description Simple method to access an incrementing value. Used for `id` allocation on all Two.js objects.
* @returns {Number} Ever increasing Number.
*/
uniqueId: function() {
return count++;
}
};
// src/utils/curves.js
var Curve = {
CollinearityEpsilon: Math.pow(10, -30),
RecursionLimit: 16,
CuspLimit: 0,
Tolerance: {
distance: 0.25,
angle: 0,
epsilon: Number.EPSILON
},
// Lookup tables for abscissas and weights with values for n = 2 .. 16.
// As values are symmetric, only store half of them and adapt algorithm
// to factor in symmetry.
abscissas: [
[0.5773502691896],
[0, 0.7745966692415],
[0.3399810435849, 0.8611363115941],
[0, 0.5384693101057, 0.9061798459387],
[0.2386191860832, 0.6612093864663, 0.9324695142032],
[0, 0.4058451513774, 0.7415311855994, 0.9491079123428],
[0.1834346424956, 0.5255324099163, 0.7966664774136, 0.9602898564975],
[0, 0.3242534234038, 0.6133714327006, 0.8360311073266, 0.9681602395076],
[
0.1488743389816,
0.4333953941292,
0.679409568299,
0.865063366689,
0.9739065285172
],
[
0,
0.2695431559523,
0.5190961292068,
0.730152005574,
0.8870625997681,
0.9782286581461
],
[
0.1252334085115,
0.3678314989982,
0.5873179542866,
0.7699026741943,
0.9041172563705,
0.9815606342467
],
[
0,
0.2304583159551,
0.4484927510364,
0.6423493394403,
0.8015780907333,
0.917598399223,
0.9841830547186
],
[
0.1080549487073,
0.3191123689279,
0.5152486363582,
0.6872929048117,
0.8272013150698,
0.9284348836636,
0.9862838086968
],
[
0,
0.2011940939974,
0.3941513470776,
0.5709721726085,
0.7244177313602,
0.8482065834104,
0.9372733924007,
0.9879925180205
],
[
0.0950125098376,
0.2816035507793,
0.4580167776572,
0.6178762444026,
0.755404408355,
0.8656312023878,
0.9445750230732,
0.9894009349916
]
],
weights: [
[1],
[0.8888888888889, 0.5555555555556],
[0.6521451548625, 0.3478548451375],
[0.5688888888889, 0.4786286704994, 0.2369268850562],
[0.4679139345727, 0.3607615730481, 0.1713244923792],
[0.4179591836735, 0.3818300505051, 0.2797053914893, 0.1294849661689],
[0.3626837833784, 0.3137066458779, 0.2223810344534, 0.1012285362904],
[
0.3302393550013,
0.31234707704,
0.2606106964029,
0.1806481606949,
0.0812743883616
],
[
0.2955242247148,
0.26926671931,
0.219086362516,
0.1494513491506,
0.0666713443087
],
[
0.2729250867779,
0.2628045445102,
0.233193764592,
0.1862902109277,
0.1255803694649,
0.0556685671162
],
[
0.2491470458134,
0.2334925365384,
0.2031674267231,
0.1600783285433,
0.1069393259953,
0.0471753363865
],
[
0.2325515532309,
0.2262831802629,
0.2078160475369,
0.1781459807619,
0.1388735102198,
0.0921214998377,
0.0404840047653
],
[
0.2152638534632,
0.2051984637213,
0.1855383974779,
0.1572031671582,
0.1215185706879,
0.0801580871598,
0.0351194603318
],
[
0.2025782419256,
0.1984314853271,
0.1861610000156,
0.166269205817,
0.1395706779262,
0.1071592204672,
0.0703660474881,
0.0307532419961
],
[
0.1894506104551,
0.1826034150449,
0.169156519395,
0.1495959888166,
0.1246289712555,
0.0951585116825,
0.0622535239386,
0.0271524594118
]
]
};
function getComponentOnCubicBezier(t, a, b, c, d) {
const k = 1 - t;
return k * k * k * a + 3 * k * k * t * b + 3 * k * t * t * c + t * t * t * d;
}
function subdivide(x1, y1, x2, y2, x3, y3, x4, y4, limit) {
limit = limit || Curve.RecursionLimit;
const amount = limit + 1;
if (Math.abs(x1 - x4) < 1e-3 && Math.abs(y1 - y4) < 1e-3) {
return [new Anchor(x4, y4)];
}
const result = [];
for (let i = 0; i < amount; i++) {
const t = i / amount;
const x = getComponentOnCubicBezier(t, x1, x2, x3, x4);
const y = getComponentOnCubicBezier(t, y1, y2, y3, y4);
result.push(new Anchor(x, y));
}
return result;
}
function getCurveLength(x1, y1, x2, y2, x3, y3, x4, y4, limit) {
if (x1 === x2 && y1 === y2 && x3 === x4 && y3 === y4) {
const dx = x4 - x1;
const dy = y4 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
const ax = 9 * (x2 - x3) + 3 * (x4 - x1), bx = 6 * (x1 + x3) - 12 * x2, cx = 3 * (x2 - x1), ay = 9 * (y2 - y3) + 3 * (y4 - y1), by = 6 * (y1 + y3) - 12 * y2, cy = 3 * (y2 - y1);
function integrand(t) {
const dx = (ax * t + bx) * t + cx, dy = (ay * t + by) * t + cy;
return Math.sqrt(dx * dx + dy * dy);
}
return integrate(integrand, 0, 1, limit || Curve.RecursionLimit);
}
function getCurveBoundingBox(x1, y1, x2, y2, x3, y3, x4, y4) {
const tvalues = [];
const bounds = [[], []];
let a, b, c, t, t1, t2, b2ac, sqrtb2ac;
for (let i = 0; i < 2; ++i) {
if (i === 0) {
b = 6 * x1 - 12 * x2 + 6 * x3;
a = -3 * x1 + 9 * x2 - 9 * x3 + 3 * x4;
c = 3 * x2 - 3 * x1;
} else {
b = 6 * y1 - 12 * y2 + 6 * y3;
a = -3 * y1 + 9 * y2 - 9 * y3 + 3 * y4;
c = 3 * y2 - 3 * y1;
}
if (Math.abs(a) < 1e-3) {
if (Math.abs(b) < 1e-3) {
continue;
}
t = -c / b;
if (0 < t && t < 1) {
tvalues.push(t);
}
continue;
}
b2ac = b * b - 4 * c * a;
sqrtb2ac = Math.sqrt(b2ac);
if (b2ac < 0) {
continue;
}
t1 = (-b + sqrtb2ac) / (2 * a);
if (0 < t1 && t1 < 1) {
tvalues.push(t1);
}
t2 = (-b - sqrtb2ac) / (2 * a);
if (0 < t2 && t2 < 1) {
tvalues.push(t2);
}
}
let j = tvalues.length;
let jlen = j;
let mt;
while (j--) {
t = tvalues[j];
mt = 1 - t;
bounds[0][j] = mt * mt * mt * x1 + 3 * mt * mt * t * x2 + 3 * mt * t * t * x3 + t * t * t * x4;
bounds[1][j] = mt * mt * mt * y1 + 3 * mt * mt * t * y2 + 3 * mt * t * t * y3 + t * t * t * y4;
}
bounds[0][jlen] = x1;
bounds[1][jlen] = y1;
bounds[0][jlen + 1] = x4;
bounds[1][jlen + 1] = y4;
bounds[0].length = bounds[1].length = jlen + 2;
return {
min: { x: Math.min.apply(0, bounds[0]), y: Math.min.apply(0, bounds[1]) },
max: { x: Math.max.apply(0, bounds[0]), y: Math.max.apply(0, bounds[1]) }
};
}
function integrate(f, a, b, n) {
let x = Curve.abscissas[n - 2], w = Curve.weights[n - 2], A = 0.5 * (b - a), B = A + a, i = 0, m = n + 1 >> 1, sum = n & 1 ? w[i++] * f(B) : 0;
while (i < m) {
const Ax = A * x[i];
sum += w[i++] * (f(B + Ax) + f(B - Ax));
}
return A * sum;
}
function getCurveFromPoints(points, closed2) {
const l = points.length, last = l - 1;
for (let i = 0; i < l; i++) {
const point = points[i];
const prev = closed2 ? mod(i - 1, l) : Math.max(i - 1, 0);
const next = closed2 ? mod(i + 1, l) : Math.min(i + 1, last);
const a = points[prev];
const b = point;
const c = points[next];
getControlPoints(a, b, c);
b.command = i === 0 ? Commands.move : Commands.curve;
}
}
function getControlPoints(a, b, c) {
const a1 = Vector.angleBetween(a, b);
const a2 = Vector.angleBetween(c, b);
let d1 = Vector.distanceBetween(a, b);
let d2 = Vector.distanceBetween(c, b);
let mid = (a1 + a2) / 2;
if (d1 < 1e-3 || d2 < 1e-3) {
if (typeof b.relative === "boolean" && !b.relative) {
b.controls.left.copy(b);
b.controls.right.copy(b);
}
return b;
}
d1 *= 0.33;
d2 *= 0.33;
if (a2 < a1) {
mid += HALF_PI;
} else {
mid -= HALF_PI;
}
b.controls.left.x = Math.cos(mid) * d1;
b.controls.left.y = Math.sin(mid) * d1;
mid -= Math.PI;
b.controls.right.x = Math.cos(mid) * d2;
b.controls.right.y = Math.sin(mid) * d2;
if (typeof b.relative === "boolean" && !b.relative) {
b.controls.left.x += b.x;
b.controls.left.y += b.y;
b.controls.right.x += b.x;
b.controls.right.y += b.y;
}
return b;
}
function getReflection(a, b, relative) {
return new Vector(
2 * a.x - (b.x + a.x) - (relative ? a.x : 0),
2 * a.y - (b.y + a.y) - (relative ? a.y : 0)
);
}
function getAnchorsFromArcData(center, xAxisRotation, rx, ry, ts, td, ccw) {
const resolution = Constants.Resolution;
const anchors = [];
for (let i = 0; i < resolution; i++) {
let pct = (i + 1) / resolution;
if (ccw) {
pct = 1 - pct;
}
const theta = pct * td + ts;
const x = rx * Math.cos(theta);
const y = ry * Math.sin(theta);
const anchor2 = new Anchor(x, y);
anchor2.command = Commands.line;
anchors.push(anchor2);
}
}
// src/utils/underscore.js
var slice = Array.prototype.slice;
function isArrayLike(collection) {
if (collection === null || collection === void 0) return false;
const length = collection.length;
return typeof length == "number" && length >= 0 && length < 4294967296;
}
var _ = {
isNaN: function(obj) {
return typeof obj === "number" && obj !== +obj;
},
isElement: function(obj) {
return !!(obj && obj.nodeType === 1);
},
isObject: function(obj) {
const type = typeof obj;
return type === "function" || type === "object" && !!obj;
},
isFunction: function(obj) {
return typeof obj === "function";
},
extend: function(base) {
const sources = slice.call(arguments, 1);
for (let i = 0; i < sources.length; i++) {
const obj = sources[i];
for (let k in obj) {
base[k] = obj[k];
}
}
return base;
},
defaults: function(base) {
const sources = slice.call(arguments, 1);
for (let i = 0; i < sources.length; i++) {
const obj = sources[i];
for (let k in obj) {
if (base[k] === void 0) {
base[k] = obj[k];
}
}
}
return base;
},
each: function(obj, iteratee, context) {
const ctx = context || this;
const keys = !isArrayLike(obj) && Object.keys(obj);
const length = (keys || obj).length;
for (let i = 0; i < length; i++) {
const k = keys ? keys[i] : i;
iteratee.call(ctx, obj[k], k, obj);
}
return obj;
},
/**
* @name Two.Utils.performance
* @property {Date} - A special `Date` like object to get the current millis of the session. Used internally to calculate time between frames.
* e.g: `Utils.performance.now() // milliseconds since epoch`
*/
performance: root.performance && root.performance.now ? root.performance : Date
};
// src/utils/dom.js
var dom = {
hasEventListeners: typeof root.addEventListener === "function",
bind: function(elem, event, func, bool) {
if (this.hasEventListeners) {
elem.addEventListener(event, func, !!bool);
} else {
elem.attachEvent("on" + event, func);
}
return dom;
},
unbind: function(elem, event, func, bool) {
if (dom.hasEventListeners) {
elem.removeEventListeners(event, func, !!bool);
} else {
elem.detachEvent("on" + event, func);
}
return dom;
},
getRequestAnimationFrame: function() {
const vendors = ["ms", "moz", "webkit", "o"];
let lastTime = 0;
let request = root.requestAnimationFrame;
if (!request) {
for (let i = 0; i < vendors.length; i++) {
request = root[vendors[i] + "RequestAnimationFrame"] || request;
}
request = request || fallbackRequest;
}
function fallbackRequest(callback, element) {
const currTime = (/* @__PURE__ */ new Date()).getTime();
const timeToCall = Math.max(0, 16 - (currTime - lastTime));
const id = root.setTimeout(nextRequest, timeToCall);
lastTime = currTime + timeToCall;
function nextRequest() {
callback(currTime + timeToCall);
}
return id;
}
return request;
}
};
var temp = root.document ? root.document.createElement("div") : {};
temp.id = "help-two-load";
Object.defineProperty(dom, "temp", {
enumerable: true,
get: function() {
if (_.isElement(temp) && !root.document.head.contains(temp)) {
temp.style.display = "none";
root.document.head.appendChild(temp);
}
return temp;
}
});
// src/utils/error.js
var TwoError = class extends Error {
name = "Two.js";
message;
constructor(message) {
super();
this.message = message;
}
};
// src/utils/device-pixel-ratio.js
var devicePixelRatio = root.devicePixelRatio || 1;
function getBackingStoreRatio(ctx) {
return ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1;
}
function getRatio(ctx) {
return devicePixelRatio / getBackingStoreRatio(ctx);
}
// src/registry.js
var Registry = class {
map = {};
constructor() {
}
/**
* @name Two.Registry#add
* @function
* @param {String} id - A unique identifier.
* @param obj - Any type of variable to be registered to the directory.
* @description Adds any value to the directory. Assigned by the `id`.
*/
add(id, obj) {
this.map[id] = obj;
return this;
}
/**
* @name Two.Registry#remove
* @function
* @param {String} id - A unique identifier.
* @description Remove any value from the directory by its `id`.
*/
remove(id) {
delete this.map[id];
return this;
}
/**
* @name Two.Registry#get
* @function
* @param {String} id - A unique identifier.
* @returns {?Object} The associated value. If unavailable then `undefined` is returned.
* @description Get a registered value by its `id`.
*/
get(id) {
return this.map[id];
}
/**
* @name Two.Registry#contains
* @function
* @param {String} id - A unique identifier.
* @returns {Boolean}
* @description Convenience method to see if a value is registered to an `id` already.
*/
contains(id) {
return id in this.map;
}
};
// src/collection.js
var Collection = class extends Array {
// Warning: Multiple inheritance hack
/**
* @private
*/
#events = new Events();
// N.B: Technique to disable enumeration on object
get _events() {
return this.#events;
}
set _events(e) {
this.#events = e;
}
// Getters and setters aren't enumerable
get _bound() {
return this.#events._bound;
}
set _bound(v) {
this.#events._bound = v;
}
addEventListener() {
return this.#events.addEventListener?.apply(this, arguments);
}
on() {
return this.#events.on?.apply(this, arguments);
}
bind() {
return this.#events.bind?.apply(this, arguments);
}
removeEventListener() {
return this.#events.removeEventListener?.apply(this, arguments);
}
off() {
return this.#events.off?.apply(this, arguments);
}
unbind() {
return this.#events.unbind?.apply(this, arguments);
}
dispatchEvent() {
return this.#events.dispatchEvent?.apply(this, arguments);
}
trigger() {
return this.#events.trigger?.apply(this, arguments);
}
listen() {
return this.#events.listen?.apply(this, arguments);
}
ignore() {
return this.#events.ignore?.apply(this, arguments);
}
constructor() {
super();
if (arguments[0] && Array.isArray(arguments[0])) {
if (arguments[0].length > 0) {
this.push.apply(this, arguments[0]);
}
} else if (arguments.length > 0) {
this.push.apply(this, arguments);
}
}
pop() {
const popped = super.pop.apply(this, arguments);
this.trigger(Events.Types.remove, [popped]);
return popped;
}
shift() {
const shifted = super.shift.apply(this, arguments);
this.trigger(Events.Types.remove, [shifted]);
return shifted;
}
push() {
const pushed = super.push.apply(this, arguments);
this.trigger(Events.Types.insert, arguments);
return pushed;
}
unshift() {
const unshifted = super.unshift.apply(this, arguments);
this.trigger(Events.Types.insert, arguments);
return unshifted;
}
splice() {
const spliced = super.splice.apply(this, arguments);
this.trigger(Events.Types.remove, spliced);
if (arguments.length > 2) {
const inserted = this.slice(
arguments[0],
arguments[0] + arguments.length - 2
);
this.trigger(Events.Types.insert, inserted);
this.trigger(Events.Types.order);
}
return spliced;
}
sort() {
super.sort.apply(this, arguments);
this.trigger(Events.Types.order);
return this;
}
reverse() {
super.reverse.apply(this, arguments);
this.trigger(Events.Types.order);
return this;
}
indexOf() {
return super.indexOf.apply(this, arguments);
}
map(func, scope) {
const results = [];
for (let key = 0; key < this.length; key++) {
const value = this[key];
let result;
if (scope) {
result = func.call(scope, value, key);
} else {
result = func(value, key);
}
results.push(result);
}
return results;
}
};
// src/element.js
var Element = class _Element extends Events {
/**
* @name Two.Element#_flagId
* @private
* @property {Boolean} - Determines whether the {@link Two.Element#id} needs updating.
*/
_flagId = false;
/**
* @name Two.Element#_flagClassName
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#className} need updating.
*/
_flagClassName = false;
/**
* @name Two.Element#renderer
* @property {Object} - Object access to store relevant renderer specific variables. Warning: manipulating this object can create unintended consequences.
* @nota-bene With the {@link Two.SVGRenderer} you can access the underlying SVG element created via `shape.renderer.elem`.
*/
_renderer = {};
/**
* @name Two.Element#id
* @property {String} - Session specific unique identifier.
* @nota-bene In the {@link Two.SVGRenderer} change this to change the underlying SVG element's id too.
*/
_id = Constants.Identifier + Constants.uniqueId();
/**
* @name Two.Element#className
* @property {String} - A class to be applied to the element to be compatible with CSS styling.
* @nota-bene Only rendered to DOM in the SVG renderer.
*/
_className = "";
/**
* @name Two.Element#classList
* @property {String[]}
* @description A list of class strings stored if imported / interpreted from an SVG element.
*/
classList = [];
constructor() {
super();
for (let prop in proto3) {
Object.defineProperty(this, prop, proto3[prop]);
}
}
static Properties = ["renderer", "id", "className"];
/**
* @name Two.Element.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Element} to create a new instance
* @returns {Two.Element}
* @description Create a new {@link Two.Element} from an object notation of a {@link Two.Element}.
* @nota-bene Works in conjunction with {@link Two.Element#toObject}
*/
static fromObject(obj) {
const elem = new _Element().copy(obj);
if ("id" in obj) {
elem.id = obj.id;
}
return elem;
}
/**
* @name Two.Element#flagReset
* @function
* @description Called internally by Two.js's renderer to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagId = this._flagClassName = false;
return this;
}
copy(element) {
if (element.renderer && typeof element.renderer.type === "string") {
this.renderer.type = element.renderer.type;
}
if (typeof element.className === "string") {
this.className = element.className;
}
return this;
}
toObject() {
return {
renderer: { type: this.renderer.type },
id: this.id,
className: this.className
};
}
/**
* @name Two.Element#dispose
* @function
* @description Release the element's renderer object and detach any events.
* This cleans up renderer-specific resources and unbinds all event listeners.
*/
dispose() {
if (typeof this.unbind === "function") {
this.unbind();
}
if (this._renderer) {
if (this._renderer.elem && this._renderer.elem.parentNode) {
this._renderer.elem.parentNode.removeChild(this._renderer.elem);
delete this._renderer.elem;
}
if (this.type === "WebGLRenderer" && this.renderer.ctx) {
const gl = this.renderer.ctx;
if (this._renderer.texture) {
gl.deleteTexture(this._renderer.texture);
delete this._renderer.texture;
}
if (this._renderer.positionBuffer) {
gl.deleteBuffer(this._renderer.positionBuffer);
delete this._renderer.positionBuffer;
}
if (this._renderer.effect) {
this._renderer.effect = null;
}
}
if (this.type === "CanvasRenderer" && this._renderer.context) {
delete this._renderer.context;
}
}
const rendererType = this._renderer.type;
this._renderer = { type: rendererType };
return this;
}
};
var proto3 = {
renderer: {
enumerable: false,
get: function() {
return this._renderer;
}
},
id: {
enumerable: true,
get: function() {
return this._id;
},
set: function(v) {
const id = this._id;
if (v === this._id) {
return;
}
this._id = v;
this._flagId = true;
if (this.parent) {
delete this.parent.children.ids[id];
this.parent.children.ids[this._id] = this;
}
}
},
className: {
enumerable: true,
get: function() {
return this._className;
},
set: function(v) {
if (this._className !== v) {
this._flagClassName = true;
this.classList = v.split(/\s+?/);
this._className = v;
}
}
}
};
// src/effects/texture.js
var anchor;
var regex = {
video: /\.(mp4|webm|ogg)$/i,
image: /\.(jpe?g|png|gif|tiff|webp)$/i,
effect: /texture|gradient/i
};
if (root.document) {
anchor = document.createElement("a");
}
var Texture = class _Texture extends Element {
/**
* @name Two.Texture#_flagSrc
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#src} needs updating.
*/
_flagSrc = false;
/**
* @name Two.Texture#_flagImage
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#image} needs updating.
*/
_flagImage = false;
/**
* @name Two.Texture#_flagVideo
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#video} needs updating.
*/
_flagVideo = false;
/**
* @name Two.Texture#_flagLoaded
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#loaded} needs updating.
*/
_flagLoaded = false;
/**
* @name Two.Texture#_flagRepeat
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#repeat} needs updating.
*/
_flagRepeat = false;
/**
* @name Two.Texture#_flagOffset
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#offset} needs updating.
*/
_flagOffset = false;
/**
* @name Two.Texture#_flagScale
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#scale} needs updating.
*/
_flagScale = false;
/**
* @name Two.Texture#_src
* @private
* @see {@link Two.Texture#src}
*/
_src = "";
/**
* @name Two.Texture#_image
* @private
* @see {@link Two.Texture#image}
*/
_image = null;
/**
* @name Two.Texture#_loaded
* @private
* @see {@link Two.Texture#loaded}
*/
_loaded = false;
/**
* @name Two.Texture#_repeat
* @private
* @see {@link Two.Texture#repeat}
*/
_repeat = "no-repeat";
/**
* @name Two.Texture#_scale
* @private
* @see {@link Two.Texture#scale}
*/
_scale = 1;
/**
* @name Two.Texture#_offset
* @private
* @see {@link Two.Texture#offset}
*/
_offset = null;
constructor(src, callback) {
super();
for (let prop in proto4) {
Object.defineProperty(this, prop, proto4[prop]);
}
this._renderer.type = "texture";
this._renderer.flagOffset = FlagOffset.bind(this);
this._renderer.flagScale = FlagScale.bind(this);
this.loaded = false;
this.repeat = "no-repeat";
this.offset = new Vector();
if (typeof callback === "function") {
const loaded = function() {
this.unbind(Events.Types.load, loaded);
if (typeof callback === "function") {
callback();
}
}.bind(this);
this.bind(Events.Types.load, loaded);
}
if (typeof src === "string") {
this.src = src;
} else if (typeof src === "object") {
const elemString = Object.prototype.toString.call(src);
if (elemString === "[object HTMLImageElement]" || elemString === "[object HTMLCanvasElement]" || elemString === "[object HTMLVideoElement]" || elemString === "[object Image]") {
this.image = src;
}
}
this._update();
}
/**
* @name Two.Texture.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Texture}.
*/
static Properties = ["src", "loaded", "repeat", "scale", "offset", "image"];
/**
* @name Two.Texture.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Texture} to create a new instance
* @returns {Two.Texture}
* @description Create a new {@link Two.Texture} from an object notation of a {@link Two.Texture}.
* @nota-bene Works in conjunction with {@link Two.Texture#toObject}
*/
fromObject(obj) {
const texture = new _Texture().copy(obj);
if ("id" in obj) {
texture.id = obj.id;
}
return texture;
}
/**
* @name Two.Texture.RegularExpressions
* @property {Object} - A map of compatible DOM Elements categorized by media format.
*/
static RegularExpressions = regex;
/**
* @name Two.Texture.ImageRegistry
* @property {Two.Registry} - A canonical listing of image data used in a single session of Two.js.
* @nota-bene This object is used to cache image data between different textures.
*/
static ImageRegistry = new Registry();
/**
* @name Two.Texture.getAbsoluteURL
* @property {Function} - Serializes a URL as an absolute path for canonical attribution in {@link Two.Texture.ImageRegistry}.
* @param {String} path
* @returns {String} - The serialized absolute path.
*/
static getAbsoluteURL(path) {
if (!anchor) {
return path;
}
anchor.href = path;
return anchor.href;
}
/**
* @name Two.Texture.loadHeadlessBuffer
* @property {Function} - Loads an image as a buffer in headless environments.
* @param {Two.Texture} texture - The {@link Two.Texture} to be loaded.
* @param {Function} onLoad - The callback function to be triggered once the image is loaded.
* @nota-bene - This function uses node's `fs.readFileSync` to spoof the `` loading process in the browser.
*/
static loadHeadlessBuffer(texture, onLoad) {
texture.image.onload = onLoad;
texture.image.src = texture.src;
}
/**
* @name Two.Texture.getTag
* @property {Function} - Retrieves the tag name of an image, video, or canvas node.
* @param {HTMLImageElement} image - The image to infer the tag name from.
* @returns {String} - Returns the tag name of an image, video, or canvas node.
*/
static getTag(image) {
return image && image.nodeName && image.nodeName.toLowerCase() || // Headless environments
"img";
}
/**
* @name Two.Texture.getImage
* @property {Function} - Convenience function to set {@link Two.Texture#image} properties with canonical versions set in {@link Two.Texture.ImageRegistry}.
* @param {String} src - The URL path of the image.
* @returns {HTMLImageElement} - Returns either a cached version of the image or a new one that is registered in {@link Two.Texture.ImageRegistry}.
*/
static getImage(src) {
const absoluteSrc = _Texture.getAbsoluteURL(src);
if (_Texture.ImageRegistry.contains(absoluteSrc)) {
return _Texture.ImageRegistry.get(absoluteSrc);
}
let image;
if (CanvasPolyfill.Image) {
image = new CanvasPolyfill.Image();
CanvasPolyfill.shim(image, "img");
} else if (root.document) {
if (regex.video.test(absoluteSrc)) {
image = document.createElement("video");
} else {
image = document.createElement("img");
}
} else {
console.warn("Two.js: no prototypical image defined for Two.Texture");
}
image.crossOrigin = "anonymous";
image.referrerPolicy = "no-referrer";
return image;
}
/**
* @name Two.Texture.Register
* @interface
* @description A collection of functions to register different types of textures. Used internally by a {@link Two.Texture}.
*/
static Register = {
canvas: function(texture, callback) {
texture._src = "#" + texture.id;
_Texture.ImageRegistry.add(texture.src, texture.image);
if (typeof callback === "function") {
callback();
}
},
img: function(texture, callback) {
const image = texture.image;
const loaded = function(e) {
if (!CanvasPolyfill.isHeadless && image.removeEventListener && typeof image.removeEventListener === "function") {
image.removeEventListener("load", loaded, false);
image.removeEventListener("error", error, false);
}
if (typeof callback === "function") {
callback();
}
};
const error = function(e) {
if (!CanvasPolyfill.isHeadless && typeof image.removeEventListener === "function") {
image.removeEventListener("load", loaded, false);
image.removeEventListener("error", error, false);
}
throw new TwoError("unable to load " + texture.src);
};
if (typeof image.width === "number" && image.width > 0 && typeof image.height === "number" && image.height > 0) {
loaded();
} else if (!CanvasPolyfill.isHeadless && typeof image.addEventListener === "function") {
image.addEventListener("load", loaded, false);
image.addEventListener("error", error, false);
}
texture._src = _Texture.getAbsoluteURL(texture._src);
if (!CanvasPolyfill.isHeadless && image && image.getAttribute("two-src")) {
return;
}
if (!CanvasPolyfill.isHeadless) {
image.setAttribute("two-src", texture.src);
}
_Texture.ImageRegistry.add(texture.src, image);
if (CanvasPolyfill.isHeadless) {
_Texture.loadHeadlessBuffer(texture, loaded);
} else {
texture.image.src = texture.src;
}
},
video: function(texture, callback) {
if (CanvasPolyfill.isHeadless) {
throw new TwoError(
"video textures are not implemented in headless environments."
);
}
const loaded = function(e) {
texture.image.removeEventListener("canplaythrough", loaded, false);
texture.image.removeEventListener("error", error, false);
texture.image.width = texture.image.videoWidth;
texture.image.height = texture.image.videoHeight;
if (typeof callback === "function") {
callback();
}
};
const error = function(e) {
texture.image.removeEventListener("canplaythrough", loaded, false);
texture.image.removeEventListener("error", error, false);
throw new TwoError("unable to load " + texture.src);
};
texture._src = _Texture.getAbsoluteURL(texture._src);
if (!texture.image.getAttribute("two-src")) {
texture.image.setAttribute("two-src", texture.src);
_Texture.ImageRegistry.add(texture.src, texture.image);
}
if (texture.image.readyState >= 4) {
loaded();
} else {
texture.image.addEventListener("canplaythrough", loaded, false);
texture.image.addEventListener("error", error, false);
texture.image.src = texture.src;
texture.image.load();
}
}
};
/**
* @name Two.Texture.load
* @function
* @param {Two.Texture} texture - The texture to load.
* @param {Function} callback - The function to be called once the texture is loaded.
*/
static load(texture, callback) {
let image = texture.image;
let tag = _Texture.getTag(image);
if (texture._flagImage) {
if (/canvas/i.test(tag)) {
_Texture.Register.canvas(texture, callback);
} else {
texture._src = !CanvasPolyfill.isHeadless && image.getAttribute("two-src") || image.src;
_Texture.Register[tag](texture, callback);
}
}
if (texture._flagSrc) {
if (!image) {
image = _Texture.getImage(texture.src);
texture.image = image;
}
tag = _Texture.getTag(image);
_Texture.Register[tag](texture, callback);
}
}
/**
* @name Two.Texture#clone
* @function
* @returns {Two.Texture}
* @description Create a new instance of {@link Two.Texture} with the same properties of the current texture.
*/
clone() {
const clone = new _Texture(this.src);
clone.repeat = this.repeat;
clone.offset.copy(this.offset);
clone.scale = this.scale;
return clone;
}
/**
* @name Two.Texture#copy
* @function
* @param {Two.Texture} texture - The reference {@link Two.Texture}
* @description Copy the properties of one {@link Two.Texture} onto another.
*/
copy(texture) {
this.src = texture.src;
this.repeat = texture.repeat;
this.offset = typeof texture.offset === "number" || texture.offset instanceof Vector ? texture.offset : new Vector().copy(texture.offset);
this.scale = typeof texture.scale === "number" || texture.scale instanceof Vector ? texture.scale : new Vector().copy(texture.scale);
return this;
}
/**
* @name Two.Texture#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the texture.
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = "texture";
result.src = this.src;
result.repeat = this.repeat;
result.offset = this.offset.toObject();
result.scale = typeof this.scale === "number" ? this.scale : this.scale.toObject();
return result;
}
/**
* @name Two.Texture#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagSrc || this._flagImage) {
this.trigger(Events.Types.change);
if (this._flagSrc || this._flagImage) {
this.loaded = false;
_Texture.load(
this,
function() {
this.loaded = true;
this.trigger(Events.Types.change).trigger(Events.Types.load);
}.bind(this)
);
}
}
if (this._image && this._image.readyState >= 4) {
this._flagVideo = true;
}
return this;
}
/**
* @name Two.Texture#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagSrc = this._flagImage = this._flagLoaded = this._flagRepeat = this._flagVideo = this._flagScale = this._flagOffset = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Texture#dispose
* @function
* @description Detach instance from renderer including any `` or textures stored in memory.
*/
dispose() {
super.dispose();
if ("elem" in this._renderer) {
const elem = this._renderer.elem;
elem.parentNode.removeChild(elem);
}
if ("effect" in this._renderer) {
this._renderer.effect = null;
}
return this;
}
};
var proto4 = {
src: {
enumerable: true,
get: function() {
return this._src;
},
set: function(v) {
this._src = v;
this._flagSrc = true;
}
},
loaded: {
enumerable: true,
get: function() {
return this._loaded;
},
set: function(v) {
this._loaded = v;
this._flagLoaded = true;
}
},
repeat: {
enumerable: true,
get: function() {
return this._repeat;
},
set: function(v) {
this._repeat = v;
this._flagRepeat = true;
}
},
image: {
enumerable: true,
get: function() {
return this._image;
},
set: function(image) {
const tag = Texture.getTag(image);
let index;
switch (tag) {
case "canvas":
index = "#" + image.id;
break;
default:
index = image.src;
}
if (Texture.ImageRegistry.contains(index)) {
this._image = Texture.ImageRegistry.get(image.src);
} else {
this._image = image;
}
this._flagImage = true;
}
},
offset: {
enumerable: true,
get: function() {
return this._offset;
},
set: function(v) {
if (this._offset) {
this._offset.unbind(Events.Types.change, this._renderer.flagOffset);
}
this._offset = v;
this._offset.bind(Events.Types.change, this._renderer.flagOffset);
this._flagOffset = true;
}
},
scale: {
enumerable: true,
get: function() {
return this._scale;
},
set: function(v) {
if (this._scale instanceof Vector) {
this._scale.unbind(Events.Types.change, this._renderer.flagScale);
}
this._scale = v;
if (this._scale instanceof Vector) {
this._scale.bind(Events.Types.change, this._renderer.flagScale);
}
this._flagScale = true;
}
}
};
function FlagOffset() {
this._flagOffset = true;
}
function FlagScale() {
this._flagScale = true;
}
// src/effects/stop.js
var Stop = class _Stop extends Element {
/**
* @name Two.Stop#_flagOffset
* @private
* @property {Boolean} - Determines whether the {@link Two.Stop#offset} needs updating.
*/
_flagOffset = true;
/**
* @name Two.Stop#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Stop#opacity} needs updating.
*/
_flagOpacity = true;
/**
* @name Two.Stop#_flagColor
* @private
* @property {Boolean} - Determines whether the {@link Two.Stop#color} needs updating.
*/
_flagColor = true;
/**
* @name Two.Stop#_offset
* @private
* @see {@link Two.Stop#offset}
*/
_offset = 0;
/**
* @name Two.Stop#_opacity
* @private
* @see {@link Two.Stop#opacity}
*/
_opacity = 1;
/**
* @name Two.Stop#_color
* @private
* @see {@link Two.Stop#color}
*/
_color = "#fff";
constructor(offset, color, opacity) {
super();
for (let prop in proto5) {
Object.defineProperty(this, prop, proto5[prop]);
}
this._renderer.type = "stop";
this.offset = typeof offset === "number" ? offset : _Stop.Index <= 0 ? 0 : 1;
this.opacity = typeof opacity === "number" ? opacity : 1;
this.color = typeof color === "string" ? color : _Stop.Index <= 0 ? "#fff" : "#000";
_Stop.Index = (_Stop.Index + 1) % 2;
}
/**
* @name Two.Stop.Index
* @property {Number} - The current index being referenced for calculating a stop's default offset value.
*/
static Index = 0;
/**
* @name Two.Stop.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Stop}.
*/
static Properties = ["offset", "opacity", "color"];
/**
* @name Two.Stop.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Stop} to create a new instance
* @returns {Two.Stop}
* @description Create a new {@link Two.Stop} from an object notation of a {@link Two.Stop}.
* @nota-bene Works in conjunction with {@link Two.Stop#toObject}
*/
static fromObject(obj) {
const stop = new _Stop().copy(obj);
if ("id" in obj) {
stop.id = obj.id;
}
return stop;
}
/**
* @name Two.Stop#copy
* @function
* @param {Two.Stop} stop - The reference {@link Two.Stop}
* @description Copy the properties of one {@link Two.Stop} onto another.
*/
copy(stop) {
super.copy.call(this, stop);
for (let i = 0; i < _Stop.Properties.length; i++) {
const k = _Stop.Properties[i];
if (k in stop) {
this[k] = stop[k];
}
}
return this;
}
/**
* @name Two.Stop#clone
* @function
* @param {Two.Gradient} [parent] - The parent gradient to add the clone to.
* @returns {Two.Stop}
* @description Create a new instance of {@link Two.Stop} with the same properties of the current path.
*/
clone(parent) {
const clone = new _Stop();
_.each(
_Stop.Properties,
function(property) {
clone[property] = this[property];
},
this
);
if (parent && parent.stops) {
parent.stops.push(clone);
}
return clone;
}
/**
* @name Two.Stop#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = "stop";
_.each(
_Stop.Properties,
(k) => {
result[k] = this[k];
},
this
);
return result;
}
/**
* @name Two.Stop#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagOffset = this._flagColor = this._flagOpacity = false;
super.flagReset.call(this);
return this;
}
};
var proto5 = {
offset: {
enumerable: true,
get: function() {
return this._offset;
},
set: function(v) {
this._offset = v;
this._flagOffset = true;
if (this.parent) {
this.parent._flagStops = true;
}
}
},
opacity: {
enumerable: true,
get: function() {
return this._opacity;
},
set: function(v) {
this._opacity = v;
this._flagOpacity = true;
if (this.parent) {
this.parent._flagStops = true;
}
}
},
color: {
enumerable: true,
get: function() {
return this._color;
},
set: function(v) {
this._color = v;
this._flagColor = true;
if (this.parent) {
this.parent._flagStops = true;
}
}
}
};
// src/effects/gradient.js
var Gradient = class _Gradient extends Element {
_flagStops = false;
_flagSpread = false;
_flagUnits = false;
_spread = "";
_units = "";
constructor(stops) {
super();
for (let prop in proto6) {
Object.defineProperty(this, prop, proto6[prop]);
}
this._renderer.type = "gradient";
this._renderer.flagStops = FlagStops.bind(this);
this._renderer.bindStops = BindStops.bind(this);
this._renderer.unbindStops = UnbindStops.bind(this);
this.spread = "pad";
this.units = "objectBoundingBox";
if (stops) {
this.stops = stops;
}
}
/**
* @name Two.Gradient.Stop
* @see {@link Two.Stop}
*/
static Stop = Stop;
/**
* @name Two.Gradient.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Gradient}.
*/
static Properties = ["spread", "stops", "units"];
/**
* @name Two.Gradient.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Gradient} to create a new instance
* @returns {Two.Gradient}
* @description Create a new {@link Two.Gradient} from an object notation of a {@link Two.Gradient}.
* @nota-bene Works in conjunction with {@link Two.Gradient#toObject}
*/
static fromObject(obj) {
let stops = obj.stops;
if (stops && stops.length > 0) {
stops = stops.map((o) => o instanceof Stop ? o : new Stop().copy(o));
}
const gradient = new _Gradient(stops).copy(obj);
if ("id" in obj) {
gradient.id = obj.id;
}
return gradient;
}
/**
* @name Two.Gradient#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Gradient}
* @description Create a new instance of {@link Two.Gradient} with the same properties of the current path.
*/
clone(parent) {
const stops = this.stops.map((s) => {
return s.clone();
});
const clone = new _Gradient(stops);
_.each(
_Gradient.Properties,
(k) => {
clone[k] = this[k];
},
this
);
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Gradient#copy
* @function
* @param {Two.Gradient} gradient - The reference {@link Two.Gradient}
* @description Copy the properties of one {@link Two.Gradient} onto another.
*/
copy(gradient) {
super.copy.call(this, gradient);
for (let i = 0; i < _Gradient.Properties.length; i++) {
const k = _Gradient.Properties[i];
if (k in gradient) {
this[k] = gradient[k];
}
}
return this;
}
/**
* @name Two.Gradient#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const result = {
stops: this.stops.map((s) => {
return s.toObject();
})
};
_.each(
_Gradient.Properties,
(k) => {
result[k] = this[k];
},
this
);
return result;
}
/**
* @name Two.Gradient#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagSpread || this._flagStops) {
this.trigger(Events.Types.change);
}
return this;
}
/**
* @name Two.Gradient#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagSpread = this._flagUnits = this._flagStops = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Gradient#dispose
* @function
* @description Detach instance from renderer including any `` or textures stored in memory.
*/
dispose() {
if ("elem" in this._renderer) {
const elem = this._renderer.elem;
elem.parentNode.removeChild(elem);
}
if ("effect" in this._renderer) {
this._renderer.effect = null;
}
return this;
}
};
var proto6 = {
spread: {
enumerable: true,
get: function() {
return this._spread;
},
set: function(v) {
this._spread = v;
this._flagSpread = true;
}
},
units: {
enumerable: true,
get: function() {
return this._units;
},
set: function(v) {
this._units = v;
this._flagUnits = true;
}
},
stops: {
enumerable: true,
get: function() {
return this._stops;
},
set: function(stops) {
const bindStops = this._renderer.bindStops;
const unbindStops = this._renderer.unbindStops;
if (this._stops) {
this._stops.unbind(Events.Types.insert, bindStops).unbind(Events.Types.remove, unbindStops);
}
this._stops = new Collection((stops || []).slice(0));
this._stops.bind(Events.Types.insert, bindStops).bind(Events.Types.remove, unbindStops);
bindStops(this._stops);
}
}
};
function FlagStops() {
this._flagStops = true;
}
function BindStops(items) {
let i = items.length;
while (i--) {
items[i].bind(Events.Types.change, this._renderer.flagStops);
items[i].parent = this;
}
this._renderer.flagStops();
}
function UnbindStops(items) {
let i = items.length;
while (i--) {
items[i].unbind(Events.Types.change, this._renderer.flagStops);
delete items[i].parent;
}
this._renderer.flagStops();
}
// src/effects/linear-gradient.js
var LinearGradient = class _LinearGradient extends Gradient {
/**
* @name Two.LinearGradient#_flagEndPoints
* @private
* @property {Boolean} - Determines whether the {@link Two.LinearGradient#left} or {@link Two.LinearGradient#right} changed and needs to update.
*/
_flagEndPoints = false;
_left = null;
_right = null;
constructor(x1, y1, x2, y2, stops) {
super(stops);
for (let prop in proto7) {
Object.defineProperty(this, prop, proto7[prop]);
}
this._renderer.type = "linear-gradient";
this._renderer.flagEndPoints = FlagEndPoints.bind(this);
this.left = new Vector();
this.right = new Vector();
if (typeof x1 === "number") {
this.left.x = x1;
}
if (typeof y1 === "number") {
this.left.y = y1;
}
if (typeof x2 === "number") {
this.right.x = x2;
}
if (typeof y2 === "number") {
this.right.y = y2;
}
}
/**
* @name Two.LinearGradient.Stop
* @see {@link Two.Stop}
*/
static Stop = Stop;
static Properties = ["left", "right"];
/**
* @name Two.LinearGradient.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.LinearGradient} to create a new instance
* @returns {Two.LinearGradient}
* @description Create a new {@link Two.LinearGradient} from an object notation of a {@link Two.LinearGradient}.
* @nota-bene Works in conjunction with {@link Two.LinearGradient#toObject}
*/
static fromObject(obj) {
const gradient = new _LinearGradient().copy(obj);
if ("id" in obj) {
gradient.id = obj.id;
}
return gradient;
}
/**
* @name Two.LinearGradient#copy
* @function
* @param {Two.LinearGradient} gradient - The reference {@link Two.LinearGradient}
* @description Copy the properties of one {@link Two.LinearGradient} onto another.
*/
copy(gradient) {
super.copy.call(this, gradient);
for (let i = 0; i < _LinearGradient.Properties.length; i++) {
const k = _LinearGradient.Properties[i];
if (k in gradient) {
this[k] = gradient[k] instanceof Vector ? gradient[k] : new Vector().copy(gradient[k]);
}
}
return this;
}
/**
* @name Two.LinearGradient#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Gradient}
* @description Create a new instance of {@link Two.LinearGradient} with the same properties of the current path.
*/
clone(parent) {
const stops = this.stops.map(function(stop) {
return stop.clone();
});
const clone = new _LinearGradient(
this.left._x,
this.left._y,
this.right._x,
this.right._y,
stops
);
_.each(
Gradient.Properties,
function(k) {
clone[k] = this[k];
},
this
);
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.LinearGradient#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const result = super.toObject.call(this);
result.left = this.left.toObject();
result.right = this.right.toObject();
return result;
}
/**
* @name Two.LinearGradient#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagEndPoints || this._flagSpread || this._flagStops) {
this.trigger(Events.Types.change);
}
return this;
}
/**
* @name Two.LinearGradient#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagEndPoints = false;
super.flagReset.call(this);
return this;
}
};
var proto7 = {
left: {
enumerable: true,
get: function() {
return this._left;
},
set: function(v) {
if (this._left instanceof Vector) {
this._left.unbind(Events.Types.change, this._renderer.flagEndPoints);
}
this._left = v;
this._left.bind(Events.Types.change, this._renderer.flagEndPoints);
this._flagEndPoints = true;
}
},
right: {
enumerable: true,
get: function() {
return this._right;
},
set: function(v) {
if (this._right instanceof Vector) {
this._right.unbind(Events.Types.change, this._renderer.flagEndPoints);
}
this._right = v;
this._right.bind(Events.Types.change, this._renderer.flagEndPoints);
this._flagEndPoints = true;
}
}
};
function FlagEndPoints() {
this._flagEndPoints = true;
}
// src/effects/radial-gradient.js
var RadialGradient = class _RadialGradient extends Gradient {
/**
* @name Two.RadialGradient#_flagRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.RadialGradient#radius} changed and needs to update.
*/
_flagRadius = false;
/**
* @name Two.RadialGradient#_flagCenter
* @private
* @property {Boolean} - Determines whether the {@link Two.RadialGradient#center} changed and needs to update.
*/
_flagCenter = false;
/**
* @name Two.RadialGradient#_flagFocal
* @private
* @property {Boolean} - Determines whether the {@link Two.RadialGradient#focal} changed and needs to update.
*/
_flagFocal = false;
_radius = 0;
_center = null;
_focal = null;
constructor(cx, cy, r, stops, fx, fy) {
super(stops);
for (let prop in proto8) {
Object.defineProperty(this, prop, proto8[prop]);
}
this._renderer.type = "radial-gradient";
this._renderer.flagCenter = FlagCenter.bind(this);
this._renderer.flagFocal = FlagFocal.bind(this);
this.center = new Vector();
this.radius = typeof r === "number" ? r : 1;
this.focal = new Vector();
if (typeof cx === "number") {
this.center.x = cx;
}
if (typeof cy === "number") {
this.center.y = cy;
}
this.focal.copy(this.center);
if (typeof fx === "number") {
this.focal.x = fx;
}
if (typeof fy === "number") {
this.focal.y = fy;
}
}
/**
* @name Two.RadialGradient.Stop
* @see {@link Two.Stop}
*/
static Stop = Stop;
/**
* @name Two.RadialGradient.Properties
* @property {String[]} - A list of properties that are on every {@link Two.RadialGradient}.
*/
static Properties = ["center", "radius", "focal"];
/**
* @name Two.RadialGradient.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.RadialGradient} to create a new instance
* @returns {Two.RadialGradient}
* @description Create a new {@link Two.RadialGradient} from an object notation of a {@link Two.RadialGradient}.
* @nota-bene Works in conjunction with {@link Two.RadialGradient#toObject}
*/
static fromObject(obj) {
const gradient = new _RadialGradient().copy(obj);
if ("id" in obj) {
gradient.id = obj.id;
}
return gradient;
}
/**
* @name Two.RadialGradient#copy
* @function
* @param {Two.RadialGradient} gradient - The reference {@link Two.RadialGradient}
* @description Copy the properties of one {@link Two.RadialGradient} onto another.
*/
copy(gradient) {
super.copy.call(this, gradient);
for (let i = 0; i < _RadialGradient.Properties.length; i++) {
const k = _RadialGradient.Properties[i];
if (k in gradient) {
if (/(center|focal)i/.test(k)) {
this[k] = gradient[k] instanceof Vector ? gradient[k] : new Vector().copy(gradient[k]);
} else if (typeof gradient[k] === "number") {
this[k] = gradient[MediaKeySystemAccess];
}
}
}
return this;
}
/**
* @name Two.RadialGradient#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.RadialGradient}
* @description Create a new instance of {@link Two.RadialGradient} with the same properties of the current path.
*/
clone(parent) {
const stops = this.stops.map(function(stop) {
return stop.clone();
});
const clone = new _RadialGradient(
this.center._x,
this.center._y,
this._radius,
stops,
this.focal._x,
this.focal._y
);
_.each(
Gradient.Properties.concat(_RadialGradient.Properties),
function(k) {
clone[k] = this[k];
},
this
);
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.RadialGradient#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const result = super.toObject.call(this);
_.each(
_RadialGradient.Properties,
function(k) {
result[k] = this[k];
},
this
);
result.center = this.center.toObject();
result.focal = this.focal.toObject();
return result;
}
/**
* @name Two.RadialGradient#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagRadius || this._flatCenter || this._flagFocal || this._flagSpread || this._flagStops) {
this.trigger(Events.Types.change);
}
return this;
}
/**
* @name Two.RadialGradient#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagRadius = this._flagCenter = this._flagFocal = false;
super.flagReset.call(this);
return this;
}
};
var proto8 = {
radius: {
enumerable: true,
get: function() {
return this._radius;
},
set: function(v) {
this._radius = v;
this._flagRadius = true;
}
},
center: {
enumerable: true,
get: function() {
return this._center;
},
set: function(v) {
if (this._center) {
this._center.unbind(Events.Types.change, this._renderer.flagCenter);
}
this._center = v;
this._center.bind(Events.Types.change, this._renderer.flagCenter);
this._flagCenter = true;
}
},
focal: {
enumerable: true,
get: function() {
return this._focal;
},
set: function(v) {
if (this._focal) {
this._focal.unbind(Events.Types.change, this._renderer.flagFocal);
}
this._focal = v;
this._focal.bind(Events.Types.change, this._renderer.flagFocal);
this._flagFocal = true;
}
}
};
function FlagCenter() {
this._flagCenter = true;
}
function FlagFocal() {
this._flagFocal = true;
}
// src/utils/shape.js
function contains(path, t) {
if (t === 0 || t === 1) {
return true;
}
const length = path._length;
const target = length * t;
let elapsed = 0;
for (let i = 0; i < path._lengths.length; i++) {
const dist = path._lengths[i];
if (elapsed >= target) {
return target - elapsed >= 0;
}
elapsed += dist;
}
return false;
}
function getIdByLength(path, target) {
const total = path._length;
if (target <= 0) {
return 0;
} else if (target >= total) {
return path._lengths.length - 1;
}
for (let i = 0, sum = 0; i < path._lengths.length; i++) {
if (sum + path._lengths[i] >= target) {
target -= sum;
return Math.max(i - 1, 0) + target / path._lengths[i];
}
sum += path._lengths[i];
}
return -1;
}
function getCurveLength2(a, b, limit) {
let x1, x2, x3, x4, y1, y2, y3, y4;
const right = b.controls && b.controls.right;
const left = a.controls && a.controls.left;
x1 = b.x;
y1 = b.y;
x2 = (right || b).x;
y2 = (right || b).y;
x3 = (left || a).x;
y3 = (left || a).y;
x4 = a.x;
y4 = a.y;
if (right && b._relative) {
x2 += b.x;
y2 += b.y;
}
if (left && a._relative) {
x3 += a.x;
y3 += a.y;
}
return getCurveLength(x1, y1, x2, y2, x3, y3, x4, y4, limit);
}
function getSubdivisions(a, b, limit) {
let x1, x2, x3, x4, y1, y2, y3, y4;
const right = b.controls && b.controls.right;
const left = a.controls && a.controls.left;
x1 = b.x;
y1 = b.y;
x2 = (right || b).x;
y2 = (right || b).y;
x3 = (left || a).x;
y3 = (left || a).y;
x4 = a.x;
y4 = a.y;
if (right && b._relative) {
x2 += b.x;
y2 += b.y;
}
if (left && a._relative) {
x3 += a.x;
y3 += a.y;
}
return subdivide(x1, y1, x2, y2, x3, y3, x4, y4, limit);
}
function getEffectFromObject(obj) {
switch (obj.renderer.type) {
case "texture":
return Texture.fromObject(obj);
case "gradient":
return Gradient.fromObject(obj);
case "linear-gradient":
return LinearGradient.fromObject(obj);
case "radial-gradient":
return RadialGradient.fromObject(obj);
}
return obj;
}
// src/matrix.js
var cos = Math.cos;
var sin = Math.sin;
var tan = Math.tan;
var array = [];
var Matrix2 = class _Matrix extends Events {
/**
* @name Two.Matrix#elements
* @property {Number[]} - The underlying data stored as an array.
*/
elements = new NumArray(9);
/**
* @name Two.Matrix#manual
* @property {Boolean} - Determines whether Two.js automatically calculates the values for the matrix or if the developer intends to manage the matrix.
* @nota-bene - Setting to `true` nullifies {@link Two.Shape#translation}, {@link Two.Shape#rotation}, and {@link Two.Shape#scale}.
*/
manual = false;
constructor(a, b, c, d, e, f) {
super();
let elements = a;
if (!Array.isArray(elements)) {
elements = Array.prototype.slice.call(arguments);
}
this.identity();
if (elements.length > 0) {
this.set(elements);
}
}
//
/**
* @name Two.Matrix.Identity
* @property {Number[]} - A stored reference to the default value of a 3 x 3 matrix.
*/
static Identity = [1, 0, 0, 0, 1, 0, 0, 0, 1];
/**
* @name Two.Matrix.Multiply
* @function
* @param {Number[]} A - The first {@link Two.Matrix} to multiply
* @param {Number[]} B - The second {@link Two.Matrix} to multiply
* @param {Number[]} [C] - An optional {@link Two.Matrix} to apply the result to
* @returns {Number[]} - If an optional `C` matrix isn't passed then a new one is created and returned.
* @description Multiply two matrices together and return the result.
*/
static Multiply(A, B, C) {
if (B.length <= 3) {
const e = A;
let x, y, z;
const a = B[0] || 0, b = B[1] || 0, c = B[2] || 0;
x = e[0] * a + e[1] * b + e[2] * c;
y = e[3] * a + e[4] * b + e[5] * c;
z = e[6] * a + e[7] * b + e[8] * c;
return [x, y, z];
}
const A0 = A[0], A1 = A[1], A2 = A[2];
const A3 = A[3], A4 = A[4], A5 = A[5];
const A6 = A[6], A7 = A[7], A8 = A[8];
const B0 = B[0], B1 = B[1], B2 = B[2];
const B3 = B[3], B4 = B[4], B5 = B[5];
const B6 = B[6], B7 = B[7], B8 = B[8];
C = C || new NumArray(9);
C[0] = A0 * B0 + A1 * B3 + A2 * B6;
C[1] = A0 * B1 + A1 * B4 + A2 * B7;
C[2] = A0 * B2 + A1 * B5 + A2 * B8;
C[3] = A3 * B0 + A4 * B3 + A5 * B6;
C[4] = A3 * B1 + A4 * B4 + A5 * B7;
C[5] = A3 * B2 + A4 * B5 + A5 * B8;
C[6] = A6 * B0 + A7 * B3 + A8 * B6;
C[7] = A6 * B1 + A7 * B4 + A8 * B7;
C[8] = A6 * B2 + A7 * B5 + A8 * B8;
return C;
}
/**
* @name Two.Matrix.fromObject
* @function
* @param {Object} obj - The object notation of a Two.Matrix to create a new instance
* @returns {Two.Matrix}
* @description Create a new {@link Two.Matrix} from an object notation of a {@link Two.Matrix}.
* @nota-bene Works in conjunction with {@link Two.Matrix#toObject}
*/
static fromObject(obj) {
return new _Matrix().copy(obj);
}
/**
* @name Two.Matrix#set
* @function
* @param {Number} a - The value for element at the first column and first row
* @param {Number} b - The value for element at the second column and first row
* @param {Number} c - The value for element at the third column and first row
* @param {Number} d - The value for element at the first column and second row
* @param {Number} e - The value for element at the second column and second row
* @param {Number} f - The value for element at the third column and second row
* @param {Number} g - The value for element at the first column and third row
* @param {Number} h - The value for element at the second column and third row
* @param {Number} i - The value for element at the third column and third row
* @description Set an array of values onto the matrix. Order described in {@link Two.Matrix}.
*/
/**
* @name Two.Matrix#set
* @function
* @param {Number[]} a - The array of elements to apply
* @description Set an array of values onto the matrix. Order described in {@link Two.Matrix}.
*/
set(a, b, c, d, e, f, g, h, i) {
if (typeof b === "undefined") {
const elements = a;
a = elements[0];
b = elements[1];
c = elements[2];
d = elements[3];
e = elements[4];
f = elements[5];
g = elements[6];
h = elements[7];
i = elements[8];
}
this.elements[0] = a;
this.elements[1] = b;
this.elements[2] = c;
this.elements[3] = d;
this.elements[4] = e;
this.elements[5] = f;
this.elements[6] = g;
this.elements[7] = h;
this.elements[8] = i;
return this.trigger(Events.Types.change);
}
/**
* @name Two.Matrix#copy
* @function
* @param {Two.Matrix} m - The matrix to copy
* @description Copy the matrix of one to the current instance.
*/
copy(m) {
this.elements[0] = m.elements[0];
this.elements[1] = m.elements[1];
this.elements[2] = m.elements[2];
this.elements[3] = m.elements[3];
this.elements[4] = m.elements[4];
this.elements[5] = m.elements[5];
this.elements[6] = m.elements[6];
this.elements[7] = m.elements[7];
this.elements[8] = m.elements[8];
this.manual = m.manual;
return this.trigger(Events.Types.change);
}
/**
* @name Two.Matrix#identity
* @function
* @description Turn matrix to the identity, like resetting.
*/
identity() {
this.elements[0] = _Matrix.Identity[0];
this.elements[1] = _Matrix.Identity[1];
this.elements[2] = _Matrix.Identity[2];
this.elements[3] = _Matrix.Identity[3];
this.elements[4] = _Matrix.Identity[4];
this.elements[5] = _Matrix.Identity[5];
this.elements[6] = _Matrix.Identity[6];
this.elements[7] = _Matrix.Identity[7];
this.elements[8] = _Matrix.Identity[8];
return this.trigger(Events.Types.change);
}
/**
* @name Two.Matrix#multiply
* @function
* @param {Number} s - The scalar to be multiplied.
* @description Multiply all components of the matrix against a single scalar value.
* @overloaded
*/
/**
* @name Two.Matrix#multiply
* @function
* @param {Number} x - The `x` component to be multiplied.
* @param {Number} y - The `y` component to be multiplied.
* @param {Number} z - The `z` component to be multiplied.
* @description Multiply all components of a matrix against a 3 component vector.
* @overloaded
*/
/**
* @name Two.Matrix#multiply
* @function
* @param {Number} a - The value at the first column and first row of the matrix to be multiplied.
* @param {Number} b - The value at the second column and first row of the matrix to be multiplied.
* @param {Number} c - The value at the third column and first row of the matrix to be multiplied.
* @param {Number} d - The value at the first column and second row of the matrix to be multiplied.
* @param {Number} e - The value at the second column and second row of the matrix to be multiplied.
* @param {Number} f - The value at the third column and second row of the matrix to be multiplied.
* @param {Number} g - The value at the first column and third row of the matrix to be multiplied.
* @param {Number} h - The value at the second column and third row of the matrix to be multiplied.
* @param {Number} i - The value at the third column and third row of the matrix to be multiplied.
* @description Multiply all components of a matrix against another matrix.
* @overloaded
*/
multiply(a, b, c, d, e, f, g, h, i) {
if (typeof b === "undefined") {
this.elements[0] *= a;
this.elements[1] *= a;
this.elements[2] *= a;
this.elements[3] *= a;
this.elements[4] *= a;
this.elements[5] *= a;
this.elements[6] *= a;
this.elements[7] *= a;
this.elements[8] *= a;
return this.trigger(Events.Types.change);
}
if (typeof c === "undefined") {
c = 1;
}
if (typeof d === "undefined") {
a = a || 0;
b = b || 0;
c = c || 0;
e = this.elements;
const x = e[0] * a + e[1] * b + e[2] * c;
const y = e[3] * a + e[4] * b + e[5] * c;
const z = e[6] * a + e[7] * b + e[8] * c;
return [x, y, z];
}
const A = this.elements;
const B = [a, b, c, d, e, f, g, h, i];
const A0 = A[0], A1 = A[1], A2 = A[2];
const A3 = A[3], A4 = A[4], A5 = A[5];
const A6 = A[6], A7 = A[7], A8 = A[8];
const B0 = B[0], B1 = B[1], B2 = B[2];
const B3 = B[3], B4 = B[4], B5 = B[5];
const B6 = B[6], B7 = B[7], B8 = B[8];
this.elements[0] = A0 * B0 + A1 * B3 + A2 * B6;
this.elements[1] = A0 * B1 + A1 * B4 + A2 * B7;
this.elements[2] = A0 * B2 + A1 * B5 + A2 * B8;
this.elements[3] = A3 * B0 + A4 * B3 + A5 * B6;
this.elements[4] = A3 * B1 + A4 * B4 + A5 * B7;
this.elements[5] = A3 * B2 + A4 * B5 + A5 * B8;
this.elements[6] = A6 * B0 + A7 * B3 + A8 * B6;
this.elements[7] = A6 * B1 + A7 * B4 + A8 * B7;
this.elements[8] = A6 * B2 + A7 * B5 + A8 * B8;
return this.trigger(Events.Types.change);
}
/**
* @name Two.Matrix#inverse
* @function
* @param {Two.Matrix} [output] - The optional matrix to apply the inversion to.
* @description Return an inverted version of the matrix. If no optional one is passed a new matrix is created and returned.
*/
inverse(output) {
const a = this.elements;
output = output || new _Matrix();
const a00 = a[0], a01 = a[1], a02 = a[2];
const a10 = a[3], a11 = a[4], a12 = a[5];
const a20 = a[6], a21 = a[7], a22 = a[8];
const b01 = a22 * a11 - a12 * a21;
const b11 = -a22 * a10 + a12 * a20;
const b21 = a21 * a10 - a11 * a20;
let det = a00 * b01 + a01 * b11 + a02 * b21;
if (!det) {
return null;
}
det = 1 / det;
output.elements[0] = b01 * det;
output.elements[1] = (-a22 * a01 + a02 * a21) * det;
output.elements[2] = (a12 * a01 - a02 * a11) * det;
output.elements[3] = b11 * det;
output.elements[4] = (a22 * a00 - a02 * a20) * det;
output.elements[5] = (-a12 * a00 + a02 * a10) * det;
output.elements[6] = b21 * det;
output.elements[7] = (-a21 * a00 + a01 * a20) * det;
output.elements[8] = (a11 * a00 - a01 * a10) * det;
return output;
}
/**
* @name Two.Matrix#scale
* @function
* @param {Number} s - The one dimensional scale to apply to the matrix.
* @description Uniformly scale the transformation matrix.
*/
/**
* @name Two.Matrix#scale
* @function
* @param {Number} sx - The horizontal scale factor.
* @param {Number} sy - The vertical scale factor
* @description Scale the transformation matrix in two dimensions.
*/
scale(sx, sy) {
const l = arguments.length;
if (l <= 1) {
sy = sx;
}
return this.multiply(sx, 0, 0, 0, sy, 0, 0, 0, 1);
}
/**
* @name Two.Matrix#rotate
* @function
* @param {Number} n - The amount to rotate in Number.
* @description Rotate the matrix.
*/
rotate(n) {
const c = cos(n);
const s = sin(n);
return this.multiply(c, -s, 0, s, c, 0, 0, 0, 1);
}
/**
* @name Two.Matrix#translate
* @function
* @param {Number} x - The horizontal translation value to apply
* @param {Number} y - The vertical translation value to apply
* @description Translate the matrix to specific `x` / `y` values.
*/
translate(x, y) {
return this.multiply(1, 0, x, 0, 1, y, 0, 0, 1);
}
/**
* @name Two.Matrix#skewX
* @function
* @param {Number} n - The amount to skew
* @description Skew the matrix by an angle in the x axis direction.
*/
skewX(n) {
const a = tan(n);
return this.multiply(1, a, 0, 0, 1, 0, 0, 0, 1);
}
/**
* @name Two.Matrix#skewY
* @function
* @param {Number} n - The amount to skew
* @description Skew the matrix by an angle in the y axis direction.
*/
skewY(n) {
const a = tan(n);
return this.multiply(1, 0, 0, a, 1, 0, 0, 0, 1);
}
/**
* @name Two.Matrix#toString
* @function
* @param {Boolean} [fullMatrix=false] - Return the full 9 elements of the matrix or just 6 for 2D transformations.
* @returns {String} - The transformation matrix as a 6 component string separated by spaces.
* @description Create a transform string. Used for the Two.js rendering APIs.
*/
toString(fullMatrix) {
array.length = 0;
this.toTransformArray(fullMatrix, array);
return array.map(toFixed).join(" ");
}
/**
* @name Two.Matrix#toTransformArray
* @function
* @param {Boolean} [fullMatrix=false] - Return the full 9 elements of the matrix or just 6 in the format for 2D transformations.
* @param {Number[]} [output] - An array empty or otherwise to apply the values to.
* @description Create a transform array. Used for the Two.js rendering APIs.
*/
toTransformArray(fullMatrix, output) {
const elements = this.elements;
const hasOutput = !!output;
const a = elements[0];
const b = elements[1];
const c = elements[2];
const d = elements[3];
const e = elements[4];
const f = elements[5];
if (fullMatrix) {
const g = elements[6];
const h = elements[7];
const i = elements[8];
if (hasOutput) {
output[0] = a;
output[1] = d;
output[2] = g;
output[3] = b;
output[4] = e;
output[5] = h;
output[6] = c;
output[7] = f;
output[8] = i;
return;
}
return [a, d, g, b, e, h, c, f, i];
}
if (hasOutput) {
output[0] = a;
output[1] = d;
output[2] = b;
output[3] = e;
output[4] = c;
output[5] = f;
return;
}
return [
a,
d,
b,
e,
c,
f
// Specific format see LN:19
];
}
/**
* @name Two.Matrix#toArray
* @function
* @param {Boolean} [fullMatrix=false] - Return the full 9 elements of the matrix or just 6 for 2D transformations.
* @param {Number[]} [output] - An array empty or otherwise to apply the values to.
* @description Create a transform array. Used for the Two.js rendering APIs.
*/
toArray(fullMatrix, output) {
const elements = this.elements;
const hasOutput = !!output;
const a = elements[0];
const b = elements[1];
const c = elements[2];
const d = elements[3];
const e = elements[4];
const f = elements[5];
if (fullMatrix) {
const g = elements[6];
const h = elements[7];
const i = elements[8];
if (hasOutput) {
output[0] = a;
output[1] = b;
output[2] = c;
output[3] = d;
output[4] = e;
output[5] = f;
output[6] = g;
output[7] = h;
output[8] = i;
return;
}
return [a, b, c, d, e, f, g, h, i];
}
if (hasOutput) {
output[0] = a;
output[1] = b;
output[2] = c;
output[3] = d;
output[4] = e;
output[5] = f;
return;
}
return [a, b, c, d, e, f];
}
/**
* @name Two.Matrix#toObject
* @function
* @description Create a JSON compatible object that represents information of the matrix.
* @nota-bene Works in conjunction with {@link Two.Matrix.fromObject}
*/
toObject() {
return {
renderer: { type: "matrix" },
elements: this.toArray(true),
manual: !!this.manual
};
}
/**
* @name Two.Matrix#clone
* @function
* @description Clone the current matrix.
*/
clone() {
return new _Matrix().copy(this);
}
};
setMatrix(Matrix2);
// src/shape.js
var Shape = class _Shape extends Element {
/**
* @name Two.Shape#_flagMatrix
* @private
* @property {Boolean} - Determines whether the matrix needs updating.
*/
_flagMatrix = true;
/**
* @name Two.Shape#_flagScale
* @private
* @property {Boolean} - Determines whether the scale needs updating.
*/
_flagScale = false;
// Underlying Properties
/**
* @name Two.Shape#_matrix
* @private
* @property {Two.Matrix} - The matrix value of the shape's position, rotation, and scale.
*/
_matrix = null;
/**
* @name Two.Shape#_worldMatrix
* @private
* @property {Two.Matrix} - The matrix value of the shape's position, rotation, and scale in the scene.
*/
_worldMatrix = null;
/**
* @name Two.Shape#_position
* @private
* @property {Two.Vector} - The translation values as a {@link Two.Vector}.
*/
_position = null;
/**
* @name Two.Shape#_rotation
* @private
* @property {Number} - The rotation value in radians.
*/
_rotation = 0;
/**
* @name Two.Shape#_scale
* @private
* @property {Number|Two.Vector} - The scale value in Number. Can be a vector for non-uniform scaling.
*/
_scale = 1;
/**
* @name Two.Shape#_skewX
* @private
* @property {Number} - The rotation value in Number.
*/
_skewX = 0;
/**
* @name Two.Shape#_skewY
* @private
* @property {Number} - The rotation value in Number.
*/
_skewY = 0;
constructor() {
super();
for (let prop in proto9) {
Object.defineProperty(this, prop, proto9[prop]);
}
this._renderer.flagMatrix = FlagMatrix.bind(this);
this.isShape = true;
this.matrix = new Matrix2();
this.worldMatrix = new Matrix2();
this.position = new Vector();
this.rotation = 0;
this.scale = 1;
this.skewX = 0;
this.skewY = 0;
}
static Properties = [
"position",
"rotation",
"scale",
"skewX",
"skewY",
"matrix",
"worldMatrix"
];
/**
* @name Two.Shape.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Shape} to create a new instance
* @returns {Two.Shape}
* @description Create a new {@link Two.Shape} from an object notation of a {@link Two.Shape}.
* @nota-bene Works in conjunction with {@link Two.Shape#toObject}
*/
static fromObject(obj) {
const shape = new _Shape().copy(obj);
if ("id" in obj) {
shape.id = obj.id;
}
return shape;
}
get renderer() {
return this._renderer;
}
set renderer(v) {
this._renderer = v;
}
/**
* @name Two.Shape#translation
* @description Alias for {@link Two.Shape#position}.
*/
get translation() {
return proto9.position.get.apply(this, arguments);
}
set translation(v) {
proto9.position.set.apply(this, arguments);
}
/**
* @name Two.Shape#addTo
* @function
* @param {Two.Group} group - The parent the shape adds itself to.
* @description Convenience method to add itself to the scenegraph.
*/
addTo(group) {
group.add(this);
return this;
}
/**
* @name Two.Shape#remove
* @function
* @description Remove self from the scene / parent.
*/
remove() {
if (!this.parent) {
return this;
}
this.parent.remove(this);
return this;
}
/**
* @name Two.Shape#contains
* @function
* @param {Number} x - x coordinate to hit test against
* @param {Number} y - y coordinate to hit test against
* @param {Object} [options] - Optional options object
* @param {Boolean} [options.ignoreVisibility] - If `true`, hit test against `shape.visible = false` shapes
* @param {Number} [options.tolerance] - Padding to hit test against in pixels
* @returns {Boolean}
* @description Check to see if coordinates are within a {@link Two.Shape}'s bounding rectangle
* @nota-bene Expects *world-space coordinates* – the same pixel-space you get from the renderer (e.g., mouse `clientX`/`clientY` adjusted for the canvas’s offset and pixel ratio).
*/
contains(x, y, options) {
const opts = options || {};
const ignoreVisibility = opts.ignoreVisibility === true;
if (!ignoreVisibility && "visible" in this && this.visible === false) {
return false;
}
if (!ignoreVisibility && "opacity" in this && typeof this.opacity === "number" && this.opacity <= 0) {
return false;
}
if (typeof this.getBoundingClientRect !== "function") {
return false;
}
const tolerance = typeof opts.tolerance === "number" ? opts.tolerance : 0;
this._update(true);
const rect = this.getBoundingClientRect();
if (!rect) {
return false;
}
return x >= rect.left - tolerance && x <= rect.right + tolerance && y >= rect.top - tolerance && y <= rect.bottom + tolerance;
}
/**
* @name Two.Shape#copy
* @function
* @param {Two.Shape} shape
* @description Copy the properties of one {@link Two.Shape} onto another.
*/
copy(shape) {
super.copy.call(this, shape);
if ("position" in shape) {
if (shape.position instanceof Vector) {
this.position = shape.position;
} else {
this.position.copy(shape.position);
}
}
if ("rotation" in shape) {
this.rotation = shape.rotation;
}
if ("scale" in shape) {
this.scale = typeof shape.scale === "number" || shape.scale instanceof Vector ? shape.scale : new Vector(shape.scale.x, shape.scale.y);
}
if ("skewX" in shape) {
this.skewX = shape.skewX;
}
if ("skewY" in shape) {
this.skewY = shape.skewY;
}
if ("matrix" in shape && shape.matrix.manual) {
this.matrix.copy(shape.matrix);
this.matrix.manual = true;
}
return this;
}
/**
* @name Two.Shape#clone
* @function
* @param {Two.Group} [parent] - Optional argument to automatically add the shape to a scenegraph.
* @returns {Two.Shape}
* @description Create a new {@link Two.Shape} with the same values as the current shape.
*/
clone(parent) {
const clone = new _Shape();
clone.position.copy(this.position);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
if (parent) {
parent.add(clone);
}
return clone._update();
}
/**
* @name Two.Shape#toObject
* @function
* @description Create a JSON compatible object that represents information of the shape.
* @nota-bene Works in conjunction with {@link Two.Shape.fromObject}
*/
toObject() {
const result = super.toObject.call(this);
result.renderer = { type: "shape" };
result.isShape = true;
result.translation = this.translation.toObject();
result.rotation = this.translation.rotation;
result.scale = this.scale instanceof Vector ? this.scale.toObject() : this.scale;
result.skewX = this.skewX;
result.skewY = this.skewY;
result.matrix = this.matrix.toObject();
return result;
}
/**
* @name Two.Shape#dispose
* @function
* @description Release the shape's bound objects by unbinding relevant events.
*/
dispose() {
super.dispose();
if (typeof this.translation === "object" && typeof this.translation.unbind === "function") {
this.translation.unbind();
}
if (typeof this.scale === "object" && typeof this.scale.unbind === "function") {
this.scale.unbind();
}
}
/**
* @name Two.Shape#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update(bubbles) {
if (!this._matrix.manual && this._flagMatrix) {
this._matrix.identity().translate(this.position.x, this.position.y);
this._matrix.rotate(this.rotation);
if (this._scale instanceof Vector) {
this._matrix.scale(this._scale.x, this._scale.y);
} else {
this._matrix.scale(this._scale);
}
this._matrix.skewX(this.skewX);
this._matrix.skewY(this.skewY);
}
if (bubbles) {
if (this.parent && this.parent._update) {
this.parent._update();
}
}
return this;
}
/**
* @name Two.Shape#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagMatrix = this._flagScale = false;
super.flagReset.call(this);
return this;
}
};
var proto9 = {
position: {
enumerable: true,
get: function() {
return this._position;
},
set: function(v) {
if (this._position) {
this._position.unbind(Events.Types.change, this._renderer.flagMatrix);
}
this._position = v;
this._position.bind(Events.Types.change, this._renderer.flagMatrix);
FlagMatrix.call(this);
}
},
rotation: {
enumerable: true,
get: function() {
return this._rotation;
},
set: function(v) {
this._rotation = v;
this._flagMatrix = true;
}
},
scale: {
enumerable: true,
get: function() {
return this._scale;
},
set: function(v) {
if (this._scale instanceof Vector) {
this._scale.unbind(Events.Types.change, this._renderer.flagMatrix);
}
this._scale = v;
if (this._scale instanceof Vector) {
this._scale.bind(Events.Types.change, this._renderer.flagMatrix);
}
this._flagMatrix = true;
this._flagScale = true;
}
},
skewX: {
enumerable: true,
get: function() {
return this._skewX;
},
set: function(v) {
this._skewX = v;
this._flagMatrix = true;
}
},
skewY: {
enumerable: true,
get: function() {
return this._skewY;
},
set: function(v) {
this._skewY = v;
this._flagMatrix = true;
}
},
matrix: {
enumerable: true,
get: function() {
return this._matrix;
},
set: function(v) {
this._matrix = v;
this._flagMatrix = true;
}
},
worldMatrix: {
enumerable: true,
get: function() {
getComputedMatrix(this, this._worldMatrix);
return this._worldMatrix;
},
set: function(v) {
this._worldMatrix = v;
}
}
};
function FlagMatrix() {
this._flagMatrix = true;
}
// src/utils/hit-test.js
var TRANSPARENT_REGEX = /^(?:none|transparent)$/i;
var DEFAULT_PRECISION = 8;
var EPSILON = Number.EPSILON;
function createPoint(x, y) {
return { x, y };
}
function pointsEqual(a, b, epsilon = EPSILON) {
return Math.abs(a.x - b.x) <= epsilon && Math.abs(a.y - b.y) <= epsilon;
}
function svgAngle(ux, uy, vx, vy) {
const dot = ux * vx + uy * vy;
const len = Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy) || 1e-12;
let ang = Math.acos(Math.max(-1, Math.min(1, dot / len)));
if (ux * vy - uy * vx < 0) {
ang = -ang;
}
return ang;
}
function sampleArcPoints(prev, anchor2, precision) {
if (!prev) {
return [createPoint(anchor2.x, anchor2.y)];
}
let rx = anchor2.rx;
let ry = anchor2.ry;
if (!(rx && ry)) {
return [createPoint(anchor2.x, anchor2.y)];
}
const xAxisRotation = (anchor2.xAxisRotation || 0) * Math.PI / 180;
const largeArcFlag = anchor2.largeArcFlag ? 1 : 0;
const sweepFlag = anchor2.sweepFlag ? 1 : 0;
rx = Math.abs(rx);
ry = Math.abs(ry);
const ax = prev.x;
const ay = prev.y;
const x = anchor2.x;
const y = anchor2.y;
const dx2 = (ax - x) / 2;
const dy2 = (ay - y) / 2;
const cosRot = Math.cos(xAxisRotation);
const sinRot = Math.sin(xAxisRotation);
let x1p = cosRot * dx2 + sinRot * dy2;
let y1p = -sinRot * dx2 + cosRot * dy2;
let rxs = rx * rx;
let rys = ry * ry;
const cr = x1p * x1p / rxs + y1p * y1p / rys;
if (cr > 1) {
const s = Math.sqrt(cr);
rx *= s;
ry *= s;
rxs = rx * rx;
rys = ry * ry;
}
const dq = rxs * y1p * y1p + rys * x1p * x1p;
const pq = dq === 0 ? 0 : (rxs * rys - dq) / dq;
let q = Math.sqrt(Math.max(0, pq));
if (largeArcFlag === sweepFlag) {
q = -q;
}
const cxp = q * rx * y1p / ry;
const cyp = -q * ry * x1p / rx;
const cx = cosRot * cxp - sinRot * cyp + (ax + x) / 2;
const cy = sinRot * cxp + cosRot * cyp + (ay + y) / 2;
const startAngle = svgAngle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
const delta = svgAngle(
(x1p - cxp) / rx,
(y1p - cyp) / ry,
(-x1p - cxp) / rx,
(-y1p - cyp) / ry
) % TWO_PI;
const endAngle = startAngle + delta;
const clockwise = sweepFlag === 0;
const angleDelta = (() => {
const raw = endAngle - startAngle;
const samePoints = Math.abs(raw) < Number.EPSILON;
let deltaAngle = mod(raw, TWO_PI);
if (deltaAngle < Number.EPSILON) {
deltaAngle = samePoints ? 0 : TWO_PI;
}
if (clockwise && !samePoints) {
deltaAngle = deltaAngle === TWO_PI ? -TWO_PI : deltaAngle - TWO_PI;
}
return deltaAngle;
})();
const steps = Math.max(Constants.Resolution, Math.max(precision * 2, 1));
const points = [];
for (let i = 1; i <= steps; i++) {
const t = i / steps;
const angle = startAngle + t * angleDelta;
let px = cx + rx * Math.cos(angle);
let py = cy + ry * Math.sin(angle);
if (xAxisRotation !== 0) {
const tx = px - cx;
const ty = py - cy;
const cosR = Math.cos(xAxisRotation);
const sinR = Math.sin(xAxisRotation);
px = tx * cosR - ty * sinR + cx;
py = tx * sinR + ty * cosR + cy;
}
points.push(createPoint(px, py));
}
return points;
}
function buildPathHitParts(path, precision = DEFAULT_PRECISION) {
const polygons = [];
const segments = [];
const vertices = path._renderer && path._renderer.vertices && path._renderer.vertices.length > 0 ? path._renderer.vertices : path.vertices;
if (!vertices || vertices.length === 0) {
return { polygons, segments };
}
const limit = Math.max(1, Math.floor(precision));
let currentPolygon = null;
let firstPoint = null;
let lastPoint = null;
let prevVertex = null;
const closePolygon = (forceClose = false) => {
if (!currentPolygon) {
return;
}
if (forceClose && firstPoint && lastPoint && !pointsEqual(firstPoint, lastPoint)) {
const closingPoint = createPoint(firstPoint.x, firstPoint.y);
segments.push({ a: lastPoint, b: closingPoint });
currentPolygon.push(closingPoint);
lastPoint = closingPoint;
}
if (currentPolygon.length >= 3 && firstPoint && lastPoint && pointsEqual(firstPoint, lastPoint)) {
polygons.push(currentPolygon);
}
currentPolygon = null;
firstPoint = null;
lastPoint = null;
};
const appendPoint = (pt) => {
if (!lastPoint) {
lastPoint = pt;
if (currentPolygon) {
currentPolygon.push(pt);
}
return;
}
if (pointsEqual(lastPoint, pt)) {
return;
}
segments.push({ a: lastPoint, b: pt });
if (currentPolygon) {
currentPolygon.push(pt);
}
lastPoint = pt;
};
for (let i = 0; i < vertices.length; i++) {
const vertex = vertices[i];
const command = vertex.command || (i === 0 ? Commands.move : Commands.line);
if (command === Commands.move) {
closePolygon(false);
const pt = createPoint(vertex.x, vertex.y);
currentPolygon = [pt];
firstPoint = pt;
lastPoint = pt;
prevVertex = vertex;
continue;
}
if (!prevVertex) {
prevVertex = vertices[Math.max(i - 1, 0)];
}
if (command === Commands.line) {
appendPoint(createPoint(vertex.x, vertex.y));
} else if (command === Commands.curve) {
const subdivisions = getSubdivisions(vertex, prevVertex, limit);
for (let j = 1; j < subdivisions.length; j++) {
const sv = subdivisions[j];
appendPoint(createPoint(sv.x, sv.y));
}
appendPoint(createPoint(vertex.x, vertex.y));
} else if (command === Commands.arc) {
const arcPoints = sampleArcPoints(prevVertex, vertex, limit);
for (let j = 0; j < arcPoints.length; j++) {
appendPoint(arcPoints[j]);
}
} else if (command === Commands.close) {
closePolygon(true);
prevVertex = vertex;
continue;
} else {
appendPoint(createPoint(vertex.x, vertex.y));
}
prevVertex = vertex;
}
if (currentPolygon) {
const shouldForceClose = !!path._closed || !!path.closed || firstPoint && lastPoint && !pointsEqual(firstPoint, lastPoint);
closePolygon(shouldForceClose);
}
return { polygons, segments };
}
function pointInPolygons(polygons, x, y) {
let inside = false;
for (let i = 0; i < polygons.length; i++) {
const polygon = polygons[i];
if (!polygon || polygon.length < 3) {
continue;
}
let lastIndex = polygon.length - 1;
for (let j = 0; j < polygon.length; j++) {
const v0 = polygon[lastIndex];
const v1 = polygon[j];
const intersects = v1.y > y !== v0.y > y && x < (v0.x - v1.x) * (y - v1.y) / (v0.y - v1.y || 1e-12) + v1.x;
if (intersects) {
inside = !inside;
}
lastIndex = j;
}
}
return inside;
}
function distanceToSegmentSquared(x, y, a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
const ddx2 = x - a.x;
const ddy2 = y - a.y;
return ddx2 * ddx2 + ddy2 * ddy2;
}
const t = ((x - a.x) * dx + (y - a.y) * dy) / (dx * dx + dy * dy);
const clamped = Math.max(0, Math.min(1, t));
const cx = a.x + clamped * dx;
const cy = a.y + clamped * dy;
const ddx = x - cx;
const ddy = y - cy;
return ddx * ddx + ddy * ddy;
}
function distanceToSegments(segments, x, y) {
if (!segments || segments.length === 0) {
return Infinity;
}
let minDistance = Infinity;
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const distance = distanceToSegmentSquared(x, y, segment.a, segment.b);
if (distance < minDistance) {
minDistance = distance;
}
}
return Math.sqrt(minDistance);
}
function hasVisibleFill(shape, override) {
if (typeof override === "boolean") {
return override;
}
const fill = shape.fill;
if (!fill && fill !== 0) {
return false;
}
if (typeof fill === "string") {
return !TRANSPARENT_REGEX.test(fill);
}
return true;
}
function hasVisibleStroke(shape, override) {
const linewidth = typeof shape.linewidth === "number" ? shape.linewidth : shape._linewidth || 0;
if (typeof override === "boolean") {
return override && linewidth > 0;
}
if (!(linewidth > 0)) {
return false;
}
const stroke = shape.stroke;
if (!stroke && stroke !== 0) {
return false;
}
if (typeof stroke === "string") {
return !TRANSPARENT_REGEX.test(stroke);
}
return true;
}
function boundsContains(rect, x, y, tolerance = 0) {
if (!rect) {
return false;
}
const left = rect.left - tolerance;
const right = rect.right + tolerance;
const top = rect.top - tolerance;
const bottom = rect.bottom + tolerance;
return x >= left && x <= right && y >= top && y <= bottom;
}
// src/utils/path.js
var EPSILON2 = Number.EPSILON;
function isRelativeAnchor(anchor2) {
return !(typeof anchor2.relative === "boolean") || !!anchor2.relative;
}
function setHandleComponent(anchor2, side, dx, dy) {
const controls = anchor2.controls;
if (!controls || !controls[side]) {
return;
}
if (Math.abs(dx) < EPSILON2 && Math.abs(dy) < EPSILON2) {
if (isRelativeAnchor(anchor2)) {
controls[side].clear();
} else {
controls[side].set(anchor2.x, anchor2.y);
}
return;
}
if (isRelativeAnchor(anchor2)) {
controls[side].set(dx, dy);
} else {
controls[side].set(anchor2.x + dx, anchor2.y + dy);
}
}
function clearHandleComponent(anchor2, side) {
setHandleComponent(anchor2, side, 0, 0);
}
function getHandleOffset(anchor2, side) {
const controls = anchor2.controls;
if (!controls || !controls[side]) {
return { x: 0, y: 0 };
}
if (isRelativeAnchor(anchor2)) {
return { x: controls[side].x, y: controls[side].y };
}
return {
x: controls[side].x - anchor2.x,
y: controls[side].y - anchor2.y
};
}
function hasNonZeroHandle(anchor2, side) {
const offset = getHandleOffset(anchor2, side);
return Math.abs(offset.x) > EPSILON2 || Math.abs(offset.y) > EPSILON2;
}
function updateAnchorCommand(anchor2) {
if (anchor2.command === Commands.move || anchor2.command === Commands.close) {
return;
}
anchor2.command = hasNonZeroHandle(anchor2, "left") || hasNonZeroHandle(anchor2, "right") ? Commands.curve : Commands.line;
}
function inheritRelative(anchor2, reference) {
if (typeof reference.relative === "boolean") {
anchor2.relative = reference.relative;
}
}
function isSegmentCurved(a, b) {
return hasNonZeroHandle(b, "right") || hasNonZeroHandle(a, "left") || hasNonZeroHandle(a, "right") || hasNonZeroHandle(b, "left") || a.command === Commands.curve || b.command === Commands.curve;
}
function lerpPoint(a, b, t) {
return {
x: lerp(a.x, b.x, t),
y: lerp(a.y, b.y, t)
};
}
function getAbsoluteHandle(anchor2, side) {
const controls = anchor2.controls && anchor2.controls[side];
if (!controls) {
return { x: anchor2.x, y: anchor2.y };
}
if (isRelativeAnchor(anchor2)) {
return { x: anchor2.x + controls.x, y: anchor2.y + controls.y };
}
return { x: controls.x, y: controls.y };
}
function splitSubdivisionSegment(start, end, t) {
const right = start.controls && start.controls.right;
const left = end.controls && end.controls.left;
const p0 = { x: start.x, y: start.y };
const p1 = right ? getAbsoluteHandle(start, "right") : { ...p0 };
const p3 = { x: end.x, y: end.y };
const p2 = left ? getAbsoluteHandle(end, "left") : { ...p3 };
const q0 = lerpPoint(p0, p1, t);
const q1 = lerpPoint(p1, p2, t);
const q2 = lerpPoint(p2, p3, t);
const r0 = lerpPoint(q0, q1, t);
const r1 = lerpPoint(q1, q2, t);
const point = lerpPoint(r0, r1, t);
const anchor2 = new Anchor(point.x, point.y);
inheritRelative(anchor2, start);
setHandleComponent(anchor2, "left", r0.x - point.x, r0.y - point.y);
setHandleComponent(anchor2, "right", r1.x - point.x, r1.y - point.y);
anchor2.command = Commands.curve;
return {
anchor: anchor2,
startOut: q0,
endIn: q2
};
}
function applyGlobalSmooth(vertices, from, to, closed2, loop2, asymmetric) {
const length = vertices.length;
const amount = to - from + 1;
let n = amount - 1;
let padding = loop2 ? Math.min(amount, 4) : 1;
let paddingLeft = padding;
let paddingRight = padding;
if (!closed2) {
paddingLeft = Math.min(1, from);
paddingRight = Math.min(1, length - to - 1);
}
n += paddingLeft + paddingRight;
if (n <= 1) {
return;
}
const knots = new Array(n + 1);
for (let i = 0, j = from - paddingLeft; i <= n; i += 1, j += 1) {
const index = mod(j, length);
knots[i] = vertices[index];
}
let x = knots[0].x + 2 * knots[1].x;
let y = knots[0].y + 2 * knots[1].y;
let f = 2;
const n1 = n - 1;
const rx = [x];
const ry = [y];
const rf = [f];
const px = new Array(n + 1);
const py = new Array(n + 1);
for (let i = 1; i < n; i += 1) {
const internal = i < n1;
const a = internal ? 1 : asymmetric ? 1 : 2;
const b = internal ? 4 : asymmetric ? 2 : 7;
const u = internal ? 4 : asymmetric ? 3 : 8;
const v = internal ? 2 : asymmetric ? 0 : 1;
const m = a / f;
f = rf[i] = b - m;
x = rx[i] = u * knots[i].x + v * knots[i + 1].x - m * x;
y = ry[i] = u * knots[i].y + v * knots[i + 1].y - m * y;
}
px[n1] = rx[n1] / rf[n1];
py[n1] = ry[n1] / rf[n1];
for (let i = n - 2; i >= 0; i -= 1) {
px[i] = (rx[i] - px[i + 1]) / rf[i];
py[i] = (ry[i] - py[i + 1]) / rf[i];
}
px[n] = (3 * knots[n].x - px[n1]) / 2;
py[n] = (3 * knots[n].y - py[n1]) / 2;
const max5 = n - paddingRight;
for (let i = paddingLeft, j = from; i <= max5; i += 1, j += 1) {
const index = mod(j, length);
const anchor2 = vertices[index];
const hx = px[i] - anchor2.x;
const hy = py[i] - anchor2.y;
if (loop2 || i < max5) {
setHandleComponent(anchor2, "right", hx, hy);
} else {
clearHandleComponent(anchor2, "right");
}
if (loop2 || i > paddingLeft) {
setHandleComponent(anchor2, "left", -hx, -hy);
} else {
clearHandleComponent(anchor2, "left");
}
updateAnchorCommand(anchor2);
}
}
function applyCatmullRom(anchor2, prev, next, factor, clampIn, clampOut) {
const p0 = prev || anchor2;
const p1 = anchor2;
const p2 = next || anchor2;
const d1 = Vector.distanceBetween(p0, p1);
const d2 = Vector.distanceBetween(p1, p2);
const a = factor === void 0 ? 0.5 : factor;
const d1a = Math.pow(d1, a);
const d2a = Math.pow(d2, a);
const d1_2a = d1a * d1a;
const d2_2a = d2a * d2a;
if (!clampIn && prev) {
const A = 2 * d2_2a + 3 * d2a * d1a + d1_2a;
const N = 3 * d2a * (d2a + d1a);
if (N !== 0) {
const hx = (d2_2a * p0.x + A * p1.x - d1_2a * p2.x) / N - p1.x;
const hy = (d2_2a * p0.y + A * p1.y - d1_2a * p2.y) / N - p1.y;
setHandleComponent(anchor2, "left", hx, hy);
} else {
clearHandleComponent(anchor2, "left");
}
} else {
clearHandleComponent(anchor2, "left");
}
if (!clampOut && next) {
const A = 2 * d1_2a + 3 * d1a * d2a + d2_2a;
const N = 3 * d1a * (d1a + d2a);
if (N !== 0) {
const hx = (d1_2a * p2.x + A * p1.x - d2_2a * p0.x) / N - p1.x;
const hy = (d1_2a * p2.y + A * p1.y - d2_2a * p0.y) / N - p1.y;
setHandleComponent(anchor2, "right", hx, hy);
} else {
clearHandleComponent(anchor2, "right");
}
} else {
clearHandleComponent(anchor2, "right");
}
updateAnchorCommand(anchor2);
}
function applyGeometric(anchor2, prev, next, factor, clampIn, clampOut) {
if (!(prev && next)) {
if (!prev) {
clearHandleComponent(anchor2, "left");
}
if (!next) {
clearHandleComponent(anchor2, "right");
}
updateAnchorCommand(anchor2);
return;
}
const p0 = prev;
const p1 = anchor2;
const p2 = next;
const d1 = Vector.distanceBetween(p0, p1);
const d2 = Vector.distanceBetween(p1, p2);
const total = d1 + d2;
const tension = factor === void 0 ? 0.4 : factor;
const vector3 = { x: p0.x - p2.x, y: p0.y - p2.y };
if (!clampIn && total !== 0) {
const k = tension * d1 / total;
setHandleComponent(anchor2, "left", vector3.x * k, vector3.y * k);
} else {
clearHandleComponent(anchor2, "left");
}
if (!clampOut && total !== 0) {
const k = tension * d1 / total - tension;
setHandleComponent(anchor2, "right", vector3.x * k, vector3.y * k);
} else {
clearHandleComponent(anchor2, "right");
}
updateAnchorCommand(anchor2);
}
function applyLocalSmooth(vertices, from, to, closed2, loop2, options) {
const type = options.type || "catmull-rom";
const factor = options.factor;
const length = vertices.length;
for (let i = from; i <= to; i += 1) {
const index = mod(i, length);
const anchor2 = vertices[index];
if (anchor2.command === Commands.move) {
clearHandleComponent(anchor2, "left");
clearHandleComponent(anchor2, "right");
continue;
}
const prevIndex = i === from && !loop2 ? null : i - 1;
const nextIndex = i === to && !loop2 ? null : i + 1;
const prev = prevIndex === null ? null : vertices[mod(prevIndex, length)];
const next = nextIndex === null ? null : vertices[mod(nextIndex, length)];
const clampIn = prevIndex === null;
const clampOut = nextIndex === null;
if (type === "geometric") {
applyGeometric(anchor2, prev, next, factor, clampIn, clampOut);
} else {
applyCatmullRom(anchor2, prev, next, factor, clampIn, clampOut);
}
}
}
// src/path.js
var min = Math.min;
var max = Math.max;
var ceil = Math.ceil;
var floor2 = Math.floor;
var vector = new Vector();
var hitTestMatrix = new Matrix2();
var Path = class _Path extends Shape {
/**
* @name Two.Path#_flagVertices
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#vertices} need updating.
*/
_flagVertices = true;
/**
* @name Two.Path#_flagLength
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#length} needs updating.
*/
_flagLength = true;
/**
* @name Two.Path#_flagFill
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#fill} needs updating.
*/
_flagFill = true;
/**
* @name Two.Path#_flagStroke
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#stroke} needs updating.
*/
_flagStroke = true;
/**
* @name Two.Path#_flagLinewidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#linewidth} needs updating.
*/
_flagLinewidth = true;
/**
* @name Two.Path#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#opacity} needs updating.
*/
_flagOpacity = true;
/**
* @name Two.Path#_flagVisible
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#visible} needs updating.
*/
_flagVisible = true;
/**
* @name Two.Path#_flagCap
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#cap} needs updating.
*/
_flagCap = true;
/**
* @name Two.Path#_flagJoin
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#join} needs updating.
*/
_flagJoin = true;
/**
* @name Two.Path#_flagMiter
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#miter} needs updating.
*/
_flagMiter = true;
/**
* @name Two.Path#_flagStrokeAttenuation
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#strokeAttenuation} needs updating.
*/
_flagStrokeAttenuation = true;
/**
* @name Two.Path#_flagMask
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#mask} needs updating.
*/
_flagMask = false;
/**
* @name Two.Path#_flagClip
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#clip} needs updating.
*/
_flagClip = false;
// Underlying Properties
/**
* @name Two.Path#_length
* @private
* @see {@link Two.Path#length}
*/
_length = 0;
/**
* @name Two.Path#_fill
* @private
* @see {@link Two.Path#fill}
*/
_fill = "#fff";
/**
* @name Two.Path#_stroke
* @private
* @see {@link Two.Path#stroke}
*/
_stroke = "#000";
/**
* @name Two.Path#_linewidth
* @private
* @see {@link Two.Path#linewidth}
*/
_linewidth = 1;
/**
* @name Two.Path#_opacity
* @private
* @see {@link Two.Path#opacity}
*/
_opacity = 1;
/**
* @name Two.Path#_visible
* @private
* @see {@link Two.Path#visible}
*/
_visible = true;
/**
* @name Two.Path#_cap
* @private
* @see {@link Two.Path#cap}
*/
_cap = "round";
/**
* @name Two.Path#_join
* @private
* @see {@link Two.Path#join}
*/
_join = "round";
/**
* @name Two.Path#_miter
* @private
* @see {@link Two.Path#miter}
*/
_miter = 4;
/**
* @name Two.Path#_closed
* @private
* @see {@link Two.Path#closed}
*/
_closed = true;
/**
* @name Two.Path#_curved
* @private
* @see {@link Two.Path#curved}
*/
_curved = false;
/**
* @name Two.Path#_automatic
* @private
* @see {@link Two.Path#automatic}
*/
_automatic = true;
/**
* @name Two.Path#_beginning
* @private
* @see {@link Two.Path#beginning}
*/
_beginning = 0;
/**
* @name Two.Path#_ending
* @private
* @see {@link Two.Path#ending}
*/
_ending = 1;
/**
* @name Two.Path#_mask
* @private
* @see {@link Two.Path#mask}
*/
_mask = null;
/**
* @name Two.Path#_clip
* @private
* @see {@link Two.Path#clip}
*/
_clip = false;
/**
* @name Two.Path#_dashes
* @private
* @see {@link Two.Path#dashes}
*/
_dashes = null;
/**
* @name Two.Path#_strokeAttenuation
* @private
* @see {@link Two.Path#strokeAttenuation}
*/
_strokeAttenuation = true;
constructor(vertices, closed2, curved, manual) {
super();
for (let prop in proto10) {
Object.defineProperty(this, prop, proto10[prop]);
}
this._renderer.type = "path";
this._renderer.flagVertices = FlagVertices.bind(this);
this._renderer.bindVertices = BindVertices.bind(this);
this._renderer.unbindVertices = UnbindVertices.bind(this);
this._renderer.flagFill = FlagFill.bind(this);
this._renderer.flagStroke = FlagStroke.bind(this);
this._renderer.vertices = [];
this._renderer.collection = [];
this.closed = !!closed2;
this.curved = !!curved;
this.beginning = 0;
this.ending = 1;
this.fill = "#fff";
this.stroke = "#000";
this.linewidth = 1;
this.opacity = 1;
this.className = "";
this.visible = true;
this.cap = "butt";
this.join = "miter";
this.miter = 4;
this.vertices = vertices;
this.automatic = !manual;
this.dashes = [];
this.dashes.offset = 0;
}
/**
* @name Two.Path.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Path}.
*/
static Properties = [
"fill",
"stroke",
"linewidth",
"opacity",
"visible",
"cap",
"join",
"miter",
"closed",
"curved",
"automatic",
"beginning",
"ending",
"dashes",
"strokeAttenuation"
];
static Utils = {
getCurveLength: getCurveLength2
};
/**
* @name Two.Path.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Path} to create a new instance
* @returns {Two.Path}
* @description Create a new {@link Two.Path} from an object notation of a {@link Two.Path}.
* @nota-bene Works in conjunction with {@link Two.Path#toObject}
*/
static fromObject(obj) {
const fill = typeof obj.fill === "string" ? obj.fill : getEffectFromObject(obj.fill);
const stroke = typeof obj.stroke === "string" ? obj.stroke : getEffectFromObject(obj.stroke);
const path = new _Path().copy({ ...obj, fill, stroke });
if ("id" in obj) {
path.id = obj.id;
}
return path;
}
/**
* @name Two.Path#copy
* @function
* @param {Two.Path} path - The reference {@link Two.Path}
* @description Copy the properties of one {@link Two.Path} onto another.
*/
copy(path) {
super.copy.call(this, path);
if (path.vertices) {
this.vertices = [];
for (let j = 0; j < path.vertices.length; j++) {
const v = path.vertices[j];
if (v instanceof Anchor) {
this.vertices.push(path.vertices[j].clone());
} else {
this.vertices.push(new Anchor().copy(v));
}
}
}
for (let i = 0; i < _Path.Properties.length; i++) {
const k = _Path.Properties[i];
if (k in path) {
this[k] = path[k];
}
}
return this;
}
/**
* @name Two.Path#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Path}
* @description Create a new instance of {@link Two.Path} with the same properties of the current path.
*/
clone(parent) {
const clone = new _Path();
for (let j = 0; j < this.vertices.length; j++) {
clone.vertices.push(this.vertices[j].clone());
}
for (let i = 0; i < _Path.Properties.length; i++) {
const k = _Path.Properties[i];
clone[k] = this[k];
}
clone.className = this.className;
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
if (parent) {
parent.add(clone);
}
return clone._update();
}
/**
* @name Two.Path#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
* @nota-bene Works in conjunction with {@link Two.Path.fromObject}
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = "path";
result.vertices = this.vertices.map((v) => v.toObject());
_.each(
_Path.Properties,
(k) => {
if (typeof this[k] !== "undefined") {
if (this[k].toObject) {
result[k] = this[k].toObject();
} else {
result[k] = this[k];
}
}
},
this
);
return result;
}
/**
* @name Two.Path#dispose
* @function
* @returns {Two.Path}
* @description Release the path's renderer resources and detach all events.
* This method cleans up vertices collection events, individual vertex events,
* control point events, and disposes fill/stroke effects (calling dispose()
* on Gradients and Textures for thorough cleanup) while preserving the
* renderer type for potential re-attachment to a new renderer.
*/
dispose() {
super.dispose();
if (this.vertices && typeof this.vertices.unbind === "function") {
try {
this.vertices.unbind();
} catch (e) {
}
}
if (this.vertices) {
for (let i = 0; i < this.vertices.length; i++) {
const v = this.vertices[i];
if (typeof v.unbind === "function") {
v.unbind();
}
if (v.controls) {
if (v.controls.left && typeof v.controls.left.unbind === "function") {
v.controls.left.unbind();
}
if (v.controls.right && typeof v.controls.right.unbind === "function") {
v.controls.right.unbind();
}
}
}
}
if (typeof this.fill === "object" && typeof this.fill.dispose === "function") {
this.fill.dispose();
} else if (typeof this.fill === "object" && typeof this.fill.unbind === "function") {
this.fill.unbind();
}
if (typeof this.stroke === "object" && typeof this.stroke.dispose === "function") {
this.stroke.dispose();
} else if (typeof this.stroke === "object" && typeof this.stroke.unbind === "function") {
this.stroke.unbind();
}
return this;
}
/**
* @name Two.Path#noFill
* @function
* @description Short hand method to set fill to `none`.
*/
noFill() {
this.fill = "none";
return this;
}
/**
* @name Two.Path#noStroke
* @function
* @description Short hand method to set stroke to `none`.
*/
noStroke() {
this.stroke = "none";
this.linewidth = 0;
return this;
}
/**
* @name Two.Path#corner
* @function
* @description Orient the vertices of the shape to the upper left-hand corner of the path.
*/
corner() {
const rect = this.getBoundingClientRect(true);
const hw = rect.width / 2;
const hh = rect.height / 2;
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
for (let i = 0; i < this.vertices.length; i++) {
const v = this.vertices[i];
v.x -= cx;
v.y -= cy;
v.x += hw;
v.y += hh;
}
if (this.mask) {
this.mask.translation.x -= cx;
this.mask.translation.x += hw;
this.mask.translation.y -= cy;
this.mask.translation.y += hh;
}
return this;
}
/**
* @name Two.Path#center
* @function
* @description Orient the vertices of the shape to the center of the path.
*/
center() {
const rect = this.getBoundingClientRect(true);
const cx = rect.left + rect.width / 2 - this.translation.x;
const cy = rect.top + rect.height / 2 - this.translation.y;
for (let i = 0; i < this.vertices.length; i++) {
const v = this.vertices[i];
v.x -= cx;
v.y -= cy;
}
if (this.mask) {
this.mask.translation.x -= cx;
this.mask.translation.y -= cy;
}
return this;
}
/**
* @name Two.Path#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the path.
*/
getBoundingClientRect(shallow) {
let matrix, border, l, i, v0, v1;
let left = Infinity, right = -Infinity, top = Infinity, bottom = -Infinity;
this._update(true);
matrix = shallow ? this.matrix : this.worldMatrix;
border = (this.linewidth || 0) / 2;
l = this._renderer.vertices.length;
if (this.linewidth > 0 || this.stroke && !/(transparent|none)/i.test(this.stroke)) {
if (this.matrix.manual) {
const { scaleX, scaleY } = decomposeMatrix(
matrix.elements[0],
matrix.elements[3],
matrix.elements[1],
matrix.elements[4],
matrix.elements[2],
matrix.elements[5]
);
if (typeof scaleX === "number" && typeof scaleY === "number") {
border = Math.max(scaleX, scaleY) * (this.linewidth || 0) / 2;
}
} else {
border *= typeof this.scale === "number" ? this.scale : Math.max(this.scale.x, this.scale.y);
}
}
if (l <= 0) {
return {
width: 0,
height: 0
};
}
for (i = 0; i < l; i++) {
v1 = this._renderer.vertices[i];
v0 = this._renderer.vertices[(i + l - 1) % l];
const [v0x, v0y] = matrix.multiply(v0.x, v0.y);
const [v1x, v1y] = matrix.multiply(v1.x, v1.y);
if (v0.controls && v1.controls) {
let rx = v0.controls.right.x;
let ry = v0.controls.right.y;
if (v0.relative) {
rx += v0.x;
ry += v0.y;
}
let [c0x, c0y] = matrix.multiply(rx, ry);
let lx = v1.controls.left.x;
let ly = v1.controls.left.y;
if (v1.relative) {
lx += v1.x;
ly += v1.y;
}
let [c1x, c1y] = matrix.multiply(lx, ly);
const bb = getCurveBoundingBox(v0x, v0y, c0x, c0y, c1x, c1y, v1x, v1y);
top = min(bb.min.y - border, top);
left = min(bb.min.x - border, left);
right = max(bb.max.x + border, right);
bottom = max(bb.max.y + border, bottom);
} else {
if (i <= 1) {
top = min(v0y - border, top);
left = min(v0x - border, left);
right = max(v0x + border, right);
bottom = max(v0y + border, bottom);
}
top = min(v1y - border, top);
left = min(v1x - border, left);
right = max(v1x + border, right);
bottom = max(v1y + border, bottom);
}
}
return {
top,
left,
right,
bottom,
width: right - left,
height: bottom - top
};
}
/**
* @name Two.Path#contains
* @function
* @param {Number} x - x coordinate to hit test against
* @param {Number} y - y coordinate to hit test against
* @param {Object} [options] - Optional options object
* @param {Boolean} [options.ignoreVisibility] - If `true`, hit test against `path.visible = false` shapes
* @param {Number} [options.tolerance] - Padding to hit test against in pixels
* @returns {Boolean}
* @description Check to see if coordinates are within a {@link Two.Path}'s bounding rectangle
* @nota-bene Expects *world-space coordinates* – the same pixel-space you get from the renderer (e.g., mouse `clientX`/`clientY` adjusted for the canvas’s offset and pixel ratio).
*/
contains(x, y, options) {
const opts = options || {};
const ignoreVisibility = opts.ignoreVisibility === true;
if (!ignoreVisibility && this.visible === false) {
return false;
}
if (!ignoreVisibility && typeof this.opacity === "number" && this.opacity <= 0) {
return false;
}
const tolerance = typeof opts.tolerance === "number" ? opts.tolerance : 0;
this._update(true);
const rect = this.getBoundingClientRect();
if (!rect || x < rect.left - tolerance || x > rect.right + tolerance || y < rect.top - tolerance || y > rect.bottom + tolerance) {
return false;
}
const matrix = this.worldMatrix;
const inverse = matrix && matrix.inverse(hitTestMatrix);
if (!inverse) {
return super.contains(x, y, opts);
}
const [localX, localY] = inverse.multiply(x, y, 1);
const precision = typeof opts.precision === "number" && !Number.isNaN(opts.precision) ? Math.max(1, Math.floor(opts.precision)) : 8;
const fillTest = hasVisibleFill(this, opts.fill);
const strokeTest = hasVisibleStroke(this, opts.stroke);
const { polygons, segments } = buildPathHitParts(this, precision);
if (fillTest && polygons.length > 0) {
if (pointInPolygons(polygons, localX, localY)) {
return true;
}
}
if (strokeTest && segments.length > 0) {
const linewidth = typeof this.linewidth === "number" ? this.linewidth : 0;
if (linewidth > 0) {
const distance = distanceToSegments(segments, localX, localY);
if (distance <= linewidth / 2 + tolerance) {
return true;
}
}
}
if (!fillTest && !strokeTest) {
return super.contains(x, y, opts);
}
if (fillTest && polygons.length === 0) {
return super.contains(x, y, opts);
}
return false;
}
/**
* @name Two.Path#getPointAt
* @function
* @param {Number} t - Percentage value describing where on the {@link Two.Path} to estimate and assign coordinate values.
* @param {Two.Vector} [obj] - Object to apply calculated x, y to. If none available returns new `Object`.
* @returns {Object}
* @description Given a float `t` from 0 to 1, return a point or assign a passed `obj`'s coordinates to that percentage on this {@link Two.Path}'s curve.
*/
getPointAt(t, obj) {
let ia, ib, result;
let x, x1, x2, x3, x4, y, y1, y2, y3, y4, left, right;
let target = this.length * Math.min(Math.max(t, 0), 1);
const length = this.vertices.length;
const last = length - 1;
let a = null;
let b = null;
for (let i = 0, l = this._lengths.length, sum = 0; i < l; i++) {
if (sum + this._lengths[i] >= target) {
if (this._closed) {
ia = mod(i, length);
ib = mod(i - 1, length);
if (i === 0) {
ia = ib;
ib = i;
}
} else {
ia = i;
ib = Math.min(Math.max(i - 1, 0), last);
}
a = this.vertices[ia];
b = this.vertices[ib];
target -= sum;
if (this._lengths[i] !== 0) {
t = target / this._lengths[i];
} else {
t = 0;
}
break;
}
sum += this._lengths[i];
}
if (a === null || b === null) {
return null;
}
if (!a) {
return b;
} else if (!b) {
return a;
}
right = b.controls && b.controls.right;
left = a.controls && a.controls.left;
x1 = b.x;
y1 = b.y;
x2 = (right || b).x;
y2 = (right || b).y;
x3 = (left || a).x;
y3 = (left || a).y;
x4 = a.x;
y4 = a.y;
if (right && b.relative) {
x2 += b.x;
y2 += b.y;
}
if (left && a.relative) {
x3 += a.x;
y3 += a.y;
}
x = getComponentOnCubicBezier(t, x1, x2, x3, x4);
y = getComponentOnCubicBezier(t, y1, y2, y3, y4);
const t1x = lerp(x1, x2, t);
const t1y = lerp(y1, y2, t);
const t2x = lerp(x2, x3, t);
const t2y = lerp(y2, y3, t);
const t3x = lerp(x3, x4, t);
const t3y = lerp(y3, y4, t);
const brx = lerp(t1x, t2x, t);
const bry = lerp(t1y, t2y, t);
const alx = lerp(t2x, t3x, t);
const aly = lerp(t2y, t3y, t);
if (_.isObject(obj)) {
obj.x = x;
obj.y = y;
if (obj instanceof Anchor) {
obj.controls.left.x = brx;
obj.controls.left.y = bry;
obj.controls.right.x = alx;
obj.controls.right.y = aly;
if (!(typeof obj.relative === "boolean") || obj.relative) {
obj.controls.left.x -= x;
obj.controls.left.y -= y;
obj.controls.right.x -= x;
obj.controls.right.y -= y;
}
}
obj.t = t;
return obj;
}
result = new Anchor(
x,
y,
brx - x,
bry - y,
alx - x,
aly - y,
this._curved ? Commands.curve : Commands.line
);
result.t = t;
return result;
}
/**
* @name Two.Path#plot
* @function
* @description Based on closed / curved and sorting of vertices plot where all points should be and where the respective handles should be too.
* @nota-bene While this method is public it is internally called by {@link Two.Path#_update} when `automatic = true`.
*/
plot() {
if (this.curved) {
getCurveFromPoints(this._collection, this.closed);
return this;
}
for (let i = 0; i < this._collection.length; i++) {
this._collection[i].command = i === 0 ? Commands.move : Commands.line;
}
return this;
}
/**
* @name Two.Path#smooth
* @function
* @param {Object} [options] - Configuration for smoothing.
* @param {String} [options.type='continuous'] - Type of smoothing algorithm.
* @param {Number} [options.from=0] - Index of vertices to start smoothing
* @param {Number} [options.to=1] - Index of vertices to terminate smoothing
* @description Adjust vertex handles to generate smooth curves without toggling `automatic`.
*/
smooth(options) {
const opts = options || {};
const type = opts.type || "continuous";
const vertices = this._collection;
const length = vertices.length;
if (length < 2) {
return this;
}
const closed2 = this._closed || length > 0 && vertices[length - 1] && vertices[length - 1].command === Commands.close;
const resolveIndex = (value, defaultIndex) => {
if (value === void 0 || value === null) {
return defaultIndex;
}
if (typeof value === "number") {
if (closed2) {
return mod(value, length);
}
let index = value;
if (index < 0) {
index += length;
}
return Math.min(Math.max(index, 0), length - 1);
}
const idx = vertices.indexOf(value);
return idx !== -1 ? idx : defaultIndex;
};
const loop2 = closed2 && opts.from === void 0 && opts.to === void 0;
let from = resolveIndex(opts.from, 0);
let to = resolveIndex(opts.to, length - 1);
if (from > to) {
if (closed2) {
from -= length;
} else {
const temp2 = from;
from = to;
to = temp2;
}
}
const rangeLength = to - from + 1;
for (let i = 0; i < rangeLength; i += 1) {
const index = mod(from + i, length);
const anchor2 = vertices[index];
const isOpenStart = !closed2 && index === 0;
if (anchor2.command === Commands.move && !isOpenStart) {
anchor2.command = Commands.line;
}
}
if (type === "continuous" || type === "asymmetric") {
applyGlobalSmooth(
vertices,
from,
to,
closed2,
loop2,
type === "asymmetric"
);
} else if (type === "catmull-rom" || type === "geometric") {
const range = {
type,
factor: opts.factor
};
applyLocalSmooth(vertices, from, to, closed2, loop2, range);
} else {
throw new Error(
`Path.smooth does not support type "${type}". Try 'continuous', 'asymmetric', 'catmull-rom', or 'geometric'.`
);
}
this._automatic = false;
this._flagVertices = true;
this._flagLength = true;
return this;
}
/**
* @name Two.Path#subdivide
* @function
* @param {Number} limit - How many times to recurse subdivisions.
* @description Insert a {@link Two.Anchor} at the midpoint between every item in {@link Two.Path#vertices}.
*/
subdivide(limit) {
this._update();
const vertices = this.vertices;
const length = vertices.length;
if (length < 2) {
return this;
}
const points = [];
let prevOriginal = null;
let subpathStartOriginal = null;
for (let i = 0; i < length; i += 1) {
const currentOriginal = vertices[i];
if (!prevOriginal || currentOriginal.command === Commands.move) {
const clone = currentOriginal.clone();
points.push(clone);
prevOriginal = currentOriginal;
subpathStartOriginal = currentOriginal;
continue;
}
const isCurve = isSegmentCurved(currentOriginal, prevOriginal);
if (isCurve) {
const subdivided = getSubdivisions(
currentOriginal,
prevOriginal,
limit
);
const steps = subdivided.length;
const prevClone = points[points.length - 1];
let startSegment = prevClone.clone();
let endSegment = currentOriginal.clone();
let prevCloneRef = prevClone;
let prevT = 0;
if (steps <= 1) {
const currentClone = currentOriginal.clone();
points.push(currentClone);
} else {
for (let j = 1; j < steps; j += 1) {
const globalT = j / steps;
const denom = 1 - prevT;
const localT = denom <= Number.EPSILON ? globalT : (globalT - prevT) / denom;
const split = splitSubdivisionSegment(
startSegment,
endSegment,
localT
);
setHandleComponent(
prevCloneRef,
"right",
split.startOut.x - prevCloneRef.x,
split.startOut.y - prevCloneRef.y
);
const newAnchor = split.anchor;
points.push(newAnchor);
prevCloneRef = newAnchor;
startSegment = newAnchor.clone();
prevT = globalT;
setHandleComponent(
endSegment,
"left",
split.endIn.x - endSegment.x,
split.endIn.y - endSegment.y
);
}
const currentClone = currentOriginal.clone();
currentClone.controls.left.copy(endSegment.controls.left);
points.push(currentClone);
}
} else {
const subdivided = getSubdivisions(
currentOriginal,
prevOriginal,
limit
);
for (let j = 1; j < subdivided.length; j += 1) {
const anchor2 = subdivided[j];
inheritRelative(anchor2, prevOriginal);
clearHandleComponent(anchor2, "left");
clearHandleComponent(anchor2, "right");
anchor2.command = Commands.line;
points.push(anchor2);
}
const currentClone = currentOriginal.clone();
points.push(currentClone);
}
prevOriginal = currentOriginal;
if (currentOriginal.command === Commands.close) {
prevOriginal = subpathStartOriginal;
}
}
this._automatic = false;
this._curved = false;
this.vertices = points;
return this;
}
/**
* @name Two.Path#_updateLength
* @function
* @private
* @param {Number} [limit] -
* @param {Boolean} [silent=false] - If set to `true` then the path isn't updated before calculation. Useful for internal use.
* @description Recalculate the {@link Two.Path#length} value.
*/
_updateLength(limit, silent) {
if (!silent) {
this._update();
}
const length = this.vertices.length;
const last = length - 1;
const closed2 = false;
let b = this.vertices[last];
let sum = 0;
if (typeof this._lengths === "undefined") {
this._lengths = [];
}
_.each(
this.vertices,
function(a, i) {
if (i <= 0 && !closed2 || a.command === Commands.move) {
b = a;
this._lengths[i] = 0;
return;
}
this._lengths[i] = getCurveLength2(a, b, limit);
sum += this._lengths[i];
b = a;
},
this
);
this._length = sum;
this._flagLength = false;
return this;
}
/**
* @name Two.Path#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices) {
if (this._automatic) {
this.plot();
}
if (this._flagLength) {
this._updateLength(void 0, true);
}
const l = this._collection.length;
const closed2 = this._closed;
const beginning = Math.min(this._beginning, this._ending);
const ending = Math.max(this._beginning, this._ending);
const bid = getIdByLength(this, beginning * this._length);
const eid = getIdByLength(this, ending * this._length);
const low = ceil(bid);
const high = floor2(eid);
let left, right, prev, next, v, i;
this._renderer.vertices.length = 0;
for (i = 0; i < l; i++) {
if (this._renderer.collection.length <= i) {
this._renderer.collection.push(new Anchor());
}
if (i > high && !right) {
v = this._renderer.collection[i].copy(this._collection[i]);
this.getPointAt(ending, v);
v.command = this._renderer.collection[i].command;
this._renderer.vertices.push(v);
right = v;
prev = this._collection[i - 1];
if (prev && prev.controls) {
if (v.relative) {
v.controls.right.clear();
} else {
v.controls.right.copy(v);
}
if (prev.relative) {
this._renderer.collection[i - 1].controls.right.copy(prev.controls.right).lerp(Vector.zero, 1 - v.t);
} else {
this._renderer.collection[i - 1].controls.right.copy(prev.controls.right).lerp(prev, 1 - v.t);
}
}
} else if (i >= low && i <= high) {
v = this._renderer.collection[i].copy(this._collection[i]);
this._renderer.vertices.push(v);
if (i === high && contains(this, ending)) {
right = v;
if (!closed2 && right.controls) {
if (right.relative) {
right.controls.right.clear();
} else {
right.controls.right.copy(right);
}
}
} else if (i === low && contains(this, beginning)) {
left = v;
left.command = Commands.move;
if (!closed2 && left.controls) {
if (left.relative) {
left.controls.left.clear();
} else {
left.controls.left.copy(left);
}
}
}
}
}
if (low > 0 && !left) {
i = low - 1;
v = this._renderer.collection[i].copy(this._collection[i]);
this.getPointAt(beginning, v);
v.command = Commands.move;
this._renderer.vertices.unshift(v);
next = this._collection[i + 1];
if (next && next.controls) {
v.controls.left.clear();
if (next.relative) {
this._renderer.collection[i + 1].controls.left.copy(next.controls.left).lerp(Vector.zero, v.t);
} else {
vector.copy(next);
this._renderer.collection[i + 1].controls.left.copy(next.controls.left).lerp(next, v.t);
}
}
}
}
Shape.prototype._update.apply(this, arguments);
return this;
}
/**
* @name Two.Path#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagVertices = this._flagLength = this._flagFill = this._flagStroke = this._flagLinewidth = this._flagOpacity = this._flagVisible = this._flagCap = this._flagJoin = this._flagMiter = this._flagClip = this._flagStrokeAttenuation = false;
Shape.prototype.flagReset.call(this);
return this;
}
};
var proto10 = {
linewidth: {
enumerable: true,
get: function() {
return this._linewidth;
},
set: function(v) {
this._linewidth = v;
this._flagLinewidth = true;
}
},
opacity: {
enumerable: true,
get: function() {
return this._opacity;
},
set: function(v) {
this._opacity = v;
this._flagOpacity = true;
}
},
visible: {
enumerable: true,
get: function() {
return this._visible;
},
set: function(v) {
this._visible = v;
this._flagVisible = true;
}
},
cap: {
enumerable: true,
get: function() {
return this._cap;
},
set: function(v) {
this._cap = v;
this._flagCap = true;
}
},
join: {
enumerable: true,
get: function() {
return this._join;
},
set: function(v) {
this._join = v;
this._flagJoin = true;
}
},
miter: {
enumerable: true,
get: function() {
return this._miter;
},
set: function(v) {
this._miter = v;
this._flagMiter = true;
}
},
fill: {
enumerable: true,
get: function() {
return this._fill;
},
set: function(f) {
if (this._fill instanceof Gradient || this._fill instanceof LinearGradient || this._fill instanceof RadialGradient || this._fill instanceof Texture) {
this._fill.unbind(Events.Types.change, this._renderer.flagFill);
}
this._fill = f;
this._flagFill = true;
if (this._fill instanceof Gradient || this._fill instanceof LinearGradient || this._fill instanceof RadialGradient || this._fill instanceof Texture) {
this._fill.bind(Events.Types.change, this._renderer.flagFill);
}
}
},
stroke: {
enumerable: true,
get: function() {
return this._stroke;
},
set: function(f) {
if (this._stroke instanceof Gradient || this._stroke instanceof LinearGradient || this._stroke instanceof RadialGradient || this._stroke instanceof Texture) {
this._stroke.unbind(Events.Types.change, this._renderer.flagStroke);
}
this._stroke = f;
this._flagStroke = true;
if (this._stroke instanceof Gradient || this._stroke instanceof LinearGradient || this._stroke instanceof RadialGradient || this._stroke instanceof Texture) {
this._stroke.bind(Events.Types.change, this._renderer.flagStroke);
}
}
},
/**
* @name Two.Path#length
* @property {Number} - The sum of distances between all {@link Two.Path#vertices}.
*/
length: {
get: function() {
if (this._flagLength) {
this._updateLength();
}
return this._length;
}
},
closed: {
enumerable: true,
get: function() {
return this._closed;
},
set: function(v) {
this._closed = !!v;
this._flagVertices = true;
}
},
curved: {
enumerable: true,
get: function() {
return this._curved;
},
set: function(v) {
this._curved = !!v;
this._flagVertices = true;
}
},
automatic: {
enumerable: true,
get: function() {
return this._automatic;
},
set: function(v) {
if (v === this._automatic) {
return;
}
this._automatic = !!v;
const method = this._automatic ? "ignore" : "listen";
_.each(this.vertices, function(v2) {
v2[method]();
});
}
},
beginning: {
enumerable: true,
get: function() {
return this._beginning;
},
set: function(v) {
this._beginning = v;
this._flagVertices = true;
}
},
ending: {
enumerable: true,
get: function() {
return this._ending;
},
set: function(v) {
this._ending = v;
this._flagVertices = true;
}
},
vertices: {
enumerable: true,
get: function() {
return this._collection;
},
set: function(vertices) {
const bindVertices = this._renderer.bindVertices;
const unbindVertices = this._renderer.unbindVertices;
if (this._collection) {
this._collection.unbind(Events.Types.insert, bindVertices).unbind(Events.Types.remove, unbindVertices);
}
if (vertices instanceof Collection) {
this._collection = vertices;
} else {
this._collection = new Collection(vertices || []);
}
this._collection.bind(Events.Types.insert, bindVertices).bind(Events.Types.remove, unbindVertices);
bindVertices(this._collection);
}
},
/**
* @name Two.Path#mask
* @property {Two.Shape} - The shape whose alpha property becomes a clipping area for the path.
* @nota-bene This property is currently not working because of SVG spec issues found here {@link https://code.google.com/p/chromium/issues/detail?id=370951}.
*/
mask: {
enumerable: true,
get: function() {
return this._mask;
},
set: function(v) {
this._mask = v;
this._flagMask = true;
if (_.isObject(v) && !v.clip) {
v.clip = true;
}
}
},
/**
* @name Two.Path#clip
* @property {Boolean} - Tells Two.js renderer if this object represents a mask for another object (or not).
*/
clip: {
enumerable: true,
get: function() {
return this._clip;
},
set: function(v) {
this._clip = v;
this._flagClip = true;
}
},
dashes: {
enumerable: true,
get: function() {
return this._dashes;
},
set: function(v) {
if (typeof v.offset !== "number") {
v.offset = this.dashes && this._dashes.offset || 0;
}
this._dashes = v;
}
},
/**
* @name Two.Path#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
* @description When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
*/
strokeAttenuation: {
enumerable: true,
get: function() {
return this._strokeAttenuation;
},
set: function(v) {
this._strokeAttenuation = !!v;
this._flagStrokeAttenuation = true;
this._flagLinewidth = true;
}
}
};
function FlagVertices() {
this._flagVertices = true;
this._flagLength = true;
if (this.parent) {
this.parent._flagLength = true;
}
}
function BindVertices(items) {
let i = items.length;
while (i--) {
items[i].bind(Events.Types.change, this._renderer.flagVertices);
}
this._renderer.flagVertices();
}
function UnbindVertices(items) {
let i = items.length;
while (i--) {
items[i].unbind(Events.Types.change, this._renderer.flagVertices);
}
this._renderer.flagVertices();
}
function FlagFill() {
this._flagFill = true;
}
function FlagStroke() {
this._flagStroke = true;
}
// src/shapes/rectangle.js
var Rectangle = class _Rectangle extends Path {
constructor(x, y, width, height) {
const points = [
new Anchor(),
new Anchor(),
new Anchor(),
new Anchor()
// new Anchor() // TODO: Figure out how to handle this for `beginning` / `ending` animations
];
super(points, true, false, true);
this._renderer.type = "rectangle";
for (let prop in proto11) {
Object.defineProperty(this, prop, proto11[prop]);
}
this.width = typeof width === "number" ? width : 1;
this.height = typeof height === "number" ? height : 1;
this.origin = new Vector();
if (typeof x === "number") {
this.translation.x = x;
}
if (typeof y === "number") {
this.translation.y = y;
}
this._update();
}
/**
* @name Two.Rectangle.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Rectangle}.
*/
static Properties = ["width", "height", "origin"];
/**
* @name Two.Rectangle.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Rectangle} to create a new instance
* @returns {Two.Rectangle}
* @description Create a new {@link Two.Rectangle} from an object notation of a {@link Two.Rectangle}.
* @nota-bene Works in conjunction with {@link Two.Rectangle#toObject}
*/
static fromObject(obj) {
const rectangle = new _Rectangle().copy(obj);
if ("id" in obj) {
rectangle.id = obj.id;
}
return rectangle;
}
/**
* @name Two.Rectangle#copy
* @function
* @param {Two.Rectangle} rectangle - The reference {@link Two.Rectangle}
* @description Copy the properties of one {@link Two.Rectangle} onto another.
*/
copy(rectangle) {
super.copy.call(this, rectangle);
for (let i = 0; i < _Rectangle.Properties.length; i++) {
const k = _Rectangle.Properties[i];
if (k in rectangle) {
if (typeof rectangle[k] === "number") {
this[k] = rectangle[k];
} else if (this[k] instanceof Vector) {
this[k].copy(rectangle[k]);
}
}
}
return this;
}
/**
* @name Two.Rectangle#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Rectangle#width} needs updating.
*/
_flagWidth = false;
/**
* @name Two.Rectangle#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Rectangle#height} needs updating.
*/
_flagHeight = false;
/**
* @name Two.Rectangle#_width
* @private
* @see {@link Two.Rectangle#width}
*/
_width = 0;
/**
* @name Two.Rectangle#_height
* @private
* @see {@link Two.Rectangle#height}
*/
_height = 0;
_origin = null;
/**
* @name Two.Rectangle#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagWidth || this._flagHeight) {
const xr = this._width / 2;
const yr = this._height / 2;
if (!this._closed && this.vertices.length === 4) {
this.vertices.push(new Anchor());
}
this.vertices[0].set(-xr, -yr).sub(this._origin).command = Commands.move;
this.vertices[1].set(xr, -yr).sub(this._origin).command = Commands.line;
this.vertices[2].set(xr, yr).sub(this._origin).command = Commands.line;
this.vertices[3].set(-xr, yr).sub(this._origin).command = Commands.line;
if (this.vertices[4]) {
this.vertices[4].set(-xr, -yr).sub(this._origin).command = Commands.line;
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Rectangle#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagWidth = this._flagHeight = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Rectangle#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Rectangle}
* @description Create a new instance of {@link Two.Rectangle} with the same properties of the current path.
*/
clone(parent) {
const clone = new _Rectangle(0, 0, this.width, this.height);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
if (clone[k] instanceof Vector) {
clone[k].copy(this[k]);
} else {
clone[k] = this[k];
}
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Rectangle#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "rectangle";
object.width = this.width;
object.height = this.height;
object.origin = this.origin.toObject();
return object;
}
};
var proto11 = {
width: {
enumerable: true,
get: function() {
return this._width;
},
set: function(v) {
this._width = v;
this._flagWidth = true;
}
},
height: {
enumerable: true,
get: function() {
return this._height;
},
set: function(v) {
this._height = v;
this._flagHeight = true;
}
},
origin: {
enumerable: true,
get: function() {
return this._origin;
},
set: function(v) {
if (this._origin) {
this._origin.unbind(Events.Types.change, this._renderer.flagVertices);
}
this._origin = v;
this._origin.bind(Events.Types.change, this._renderer.flagVertices);
this._renderer.flagVertices();
}
}
};
// src/effects/sprite.js
var Sprite = class _Sprite extends Rectangle {
/**
* @name Two.Sprite#_flagTexture
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#texture} needs updating.
*/
_flagTexture = false;
/**
* @name Two.Sprite#_flagColumns
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#columns} need updating.
*/
_flagColumns = false;
/**
* @name Two.Sprite#_flagRows
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#rows} need updating.
*/
_flagRows = false;
/**
* @name Two.Sprite#_flagFrameRate
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#flagFrameRate} needs updating.
*/
_flagFrameRate = false;
/**
* @name Two.Sprite#_flagIndex
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#index} needs updating.
*/
_flagIndex = false;
// Private variables
/**
* @name Two.Sprite#_amount
* @private
* @property {Number} - Number of frames for a given {@link Two.Sprite}.
*/
_amount = 1;
/**
* @name Two.Sprite#_duration
* @private
* @property {Number} - Number of milliseconds a {@link Two.Sprite}.
*/
_duration = 0;
/**
* @name Two.Sprite#_startTime
* @private
* @property {Milliseconds} - Epoch time in milliseconds of when the {@link Two.Sprite} started.
*/
_startTime = 0;
/**
* @name Two.Sprite#_playing
* @private
* @property {Boolean} - Dictates whether the {@link Two.Sprite} is animating or not.
*/
_playing = false;
/**
* @name Two.Sprite#_firstFrame
* @private
* @property {Number} - The frame the {@link Two.Sprite} should start with.
*/
_firstFrame = 0;
/**
* @name Two.Sprite#_lastFrame
* @private
* @property {Number} - The frame the {@link Two.Sprite} should end with.
*/
_lastFrame = 0;
/**
* @name Two.Sprite#_loop
* @private
* @property {Boolean} - Dictates whether the {@link Two.Sprite} should loop or not.
*/
_loop = true;
// Exposed through getter-setter
/**
* @name Two.Sprite#_texture
* @private
* @see {@link Two.Sprite#texture}
*/
_texture = null;
/**
* @name Two.Sprite#_columns
* @private
* @see {@link Two.Sprite#columns}
*/
_columns = 1;
/**
* @name Two.Sprite#_rows
* @private
* @see {@link Two.Sprite#rows}
*/
_rows = 1;
/**
* @name Two.Sprite#_frameRate
* @private
* @see {@link Two.Sprite#frameRate}
*/
_frameRate = 0;
/**
* @name Two.Sprite#_index
* @private
* @property {Number} - The current frame the {@link Two.Sprite} is currently displaying.
*/
_index = 0;
/**
* @name Two.Sprite#_origin
* @private
* @see {@link Two.Sprite#origin}
*/
_origin = null;
constructor(src, ox, oy, cols, rows, frameRate) {
super(ox, oy, 0, 0);
this._renderer.type = "sprite";
for (let prop in proto12) {
Object.defineProperty(this, prop, proto12[prop]);
}
this.noStroke();
this.noFill();
if (src instanceof Texture) {
this.texture = src;
} else if (typeof src === "string") {
this.texture = new Texture(src);
}
this.origin = new Vector();
this._update();
if (typeof cols === "number") {
this.columns = cols;
}
if (typeof rows === "number") {
this.rows = rows;
}
if (typeof frameRate === "number") {
this.frameRate = frameRate;
}
this.index = 0;
}
/**
* @name Two.Sprite.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Sprite}.
*/
static Properties = [
"texture",
"columns",
"rows",
"frameRate",
"index",
"firstFrame",
"lastFrame",
"loop"
];
/**
* @name Two.Sprite.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Sprite} to create a new instance
* @returns {Two.Sprite}
* @description Create a new {@link Two.Sprite} from an object notation of a {@link Two.Sprite}.
* @nota-bene Works in conjunction with {@link Two.Sprite#toObject}
*/
static fromObject(obj) {
const sprite = new _Sprite().copy(obj);
if ("id" in obj) {
sprite.id = obj.id;
}
return sprite;
}
/**
* @name Two.Sprite#copy
* @function
* @param {Two.Sprite} sprite - The reference {@link Two.Sprite}
* @description Copy the properties of one {@link Two.Sprite} onto another.
*/
copy(sprite) {
super.copy.call(this, sprite);
for (let i = 0; i < _Sprite.Properties.length; i++) {
const k = _Sprite.Properties[i];
if (k in sprite) {
this[k] = sprite[k];
}
}
return this;
}
/**
* @name Two.Sprite#play
* @function
* @param {Number} [firstFrame=0] - The index of the frame to start the animation with.
* @param {Number} [lastFrame] - The index of the frame to end the animation with. Defaults to the last item in the {@link Two.Sprite#textures}.
* @param {Function} [onLastFrame] - Optional callback function to be triggered after playing the last frame. This fires multiple times when the sprite is looped.
* @description Initiate animation playback of a {@link Two.Sprite}.
*/
play(firstFrame, lastFrame, onLastFrame) {
this._playing = true;
this._firstFrame = 0;
this._lastFrame = this.amount - 1;
this._startTime = _.performance.now();
if (typeof firstFrame === "number") {
this._firstFrame = firstFrame;
}
if (typeof lastFrame === "number") {
this._lastFrame = lastFrame;
}
if (typeof onLastFrame === "function") {
this._onLastFrame = onLastFrame;
} else {
delete this._onLastFrame;
}
if (this._index !== this._firstFrame) {
this._startTime -= 1e3 * Math.abs(this._index - this._firstFrame) / this._frameRate;
}
return this;
}
/**
* @name Two.Sprite#pause
* @function
* @description Halt animation playback of a {@link Two.Sprite}.
*/
pause() {
this._playing = false;
return this;
}
/**
* @name Two.Sprite#stop
* @function
* @description Halt animation playback of a {@link Two.Sprite} and set the current frame back to the first frame.
*/
stop() {
this._playing = false;
this._index = 0;
return this;
}
/**
* @name Two.Sprite#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Sprite}
* @description Create a new instance of {@link Two.Sprite} with the same properties of the current sprite.
*/
clone(parent) {
const clone = new _Sprite(
this.texture,
this.translation.x,
this.translation.y,
this.columns,
this.rows,
this.frameRate
);
if (this.playing) {
clone.play(this._firstFrame, this._lastFrame);
}
clone.loop = this.loop;
clone.firstFrame = this.firstFrame;
clone.lastFrame = this.lastFrame;
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Sprite#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "sprite";
object.texture = this.texture.toObject();
object.columns = this.columns;
object.rows = this.rows;
object.frameRate = this.frameRate;
object.index = this.index;
object.firstFrame = this.firstFrame;
object.lastFrame = this.lastFrame;
object.loop = this.loop;
return object;
}
/**
* @name Two.Sprite#dispose
* @function
* @returns {Two.Sprite}
* @description Release the sprite's renderer resources and detach all events.
* This method stops any running animation, clears animation callbacks, disposes
* the texture (calling dispose() for thorough cleanup), and inherits comprehensive
* cleanup from the Rectangle/Path hierarchy while preserving the renderer type
* for potential re-attachment.
*/
dispose() {
super.dispose();
if (this._playing) {
this._playing = false;
}
this._onLastFrame = null;
this._startTime = 0;
if (this._texture && typeof this._texture.dispose === "function") {
this._texture.dispose();
} else if (this._texture && typeof this._texture.unbind === "function") {
this._texture.unbind();
}
return this;
}
/**
* @name Two.Sprite#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
const effect = this._texture;
const cols = this._columns;
const rows = this._rows;
let width, height, elapsed, amount, duration;
let index, iw, ih, frames;
if (effect) {
if (this._flagColumns || this._flagRows) {
this._amount = this._columns * this._rows;
}
if (this._flagFrameRate) {
this._duration = 1e3 * this._amount / this._frameRate;
}
if (this._flagTexture) {
this.fill = effect;
}
if (effect.loaded) {
iw = effect.image.width;
ih = effect.image.height;
width = iw / cols;
height = ih / rows;
amount = this._amount;
if (this.width !== width) {
this.width = width;
}
if (this.height !== height) {
this.height = height;
}
if (this._playing && this._frameRate > 0) {
if (_.isNaN(this._lastFrame)) {
this._lastFrame = amount - 1;
}
elapsed = _.performance.now() - this._startTime;
frames = this._lastFrame + 1;
duration = 1e3 * (frames - this._firstFrame) / this._frameRate;
if (this._loop) {
elapsed = elapsed % duration;
} else {
elapsed = Math.min(elapsed, duration);
}
index = lerp(this._firstFrame, frames, elapsed / duration);
index = Math.floor(index);
if (index !== this._index) {
this._index = index;
if (index >= this._lastFrame - 1 && this._onLastFrame) {
this._onLastFrame();
}
}
}
const col = this._index % cols;
const row = Math.floor(this._index / cols);
const ox = -width * col + (iw - width) / 2;
const oy = -height * row + (ih - height) / 2;
if (ox !== effect.offset.x) {
effect.offset.x = ox;
}
if (oy !== effect.offset.y) {
effect.offset.y = oy;
}
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Sprite#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagTexture = this._flagColumns = this._flagRows = this._flagFrameRate = false;
super.flagReset.call(this);
return this;
}
};
var proto12 = {
texture: {
enumerable: true,
get: function() {
return this._texture;
},
set: function(v) {
this._texture = v;
this._flagTexture = true;
}
},
columns: {
enumerable: true,
get: function() {
return this._columns;
},
set: function(v) {
this._columns = v;
this._flagColumns = true;
}
},
rows: {
enumerable: true,
get: function() {
return this._rows;
},
set: function(v) {
this._rows = v;
this._flagRows = true;
}
},
frameRate: {
enumerable: true,
get: function() {
return this._frameRate;
},
set: function(v) {
this._frameRate = v;
this._flagFrameRate = true;
}
},
index: {
enumerable: true,
get: function() {
return this._index;
},
set: function(v) {
this._index = v;
this._flagIndex = true;
}
},
firstFrame: {
enumerable: true,
get: function() {
return this._firstFrame;
},
set: function(v) {
this._firstFrame = v;
}
},
lastFrame: {
enumerable: true,
get: function() {
return this._lastFrame;
},
set: function(v) {
this._lastFrame = v;
}
},
loop: {
enumerable: true,
get: function() {
return this._loop;
},
set: function(v) {
this._loop = !!v;
}
}
};
// src/children.js
var Children = class extends Collection {
/**
* @name Two.Group.Children#ids
* @property {Object} - Map of all elements in the list keyed by `id`s.
*/
// N.B: Technique to disable enumeration on object
#ids = {};
get ids() {
return this.#ids;
}
constructor(children) {
children = Array.isArray(children) ? children : Array.prototype.slice.call(arguments);
super(children);
this.attach(children);
this.on(Events.Types.insert, this.attach);
this.on(Events.Types.remove, this.detach);
}
/**
* @function
* @name Two.Group.Children#attach
* @param {Two.Shape[]} children - The objects which extend {@link Two.Shape} to be added.
* @description Adds elements to the `ids` map.
*/
attach(children) {
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child && child.id) {
this.ids[child.id] = child;
}
}
return this;
}
/**
* @function
* @name Two.Group.Children#detach
* @param {Two.Shape[]} children - The objects which extend {@link Two.Shape} to be removed.
* @description Removes elements to the `ids` map.
*/
detach(children) {
for (let i = 0; i < children.length; i++) {
delete this.ids[children[i].id];
}
return this;
}
};
// src/shapes/arc-segment.js
var ArcSegment = class _ArcSegment extends Path {
/**
* @name Two.ArcSegment#_flagStartAngle
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#startAngle} needs updating.
*/
_flagStartAngle = false;
/**
* @name Two.ArcSegment#_flagEndAngle
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#endAngle} needs updating.
*/
_flagEndAngle = false;
/**
* @name Two.ArcSegment#_flagInnerRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#innerRadius} needs updating.
*/
_flagInnerRadius = false;
/**
* @name Two.ArcSegment#_flagOuterRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#outerRadius} needs updating.
*/
_flagOuterRadius = false;
/**
* @name Two.ArcSegment#_startAngle
* @private
* @see {@link Two.ArcSegment#startAngle}
*/
_startAngle = 0;
/**
* @name Two.ArcSegment#_endAngle
* @private
* @see {@link Two.ArcSegment#endAngle}
*/
_endAngle = TWO_PI;
/**
* @name Two.ArcSegment#_innerRadius
* @private
* @see {@link Two.ArcSegment#innerRadius}
*/
_innerRadius = 0;
/**
* @name Two.ArcSegment#_outerRadius
* @private
* @see {@link Two.ArcSegment#outerRadius}
*/
_outerRadius = 0;
constructor(x, y, ir, or, sa, ea, res) {
const amount = res || Constants.Resolution * 3;
const points = [];
for (let i = 0; i < amount; i++) {
points.push(new Anchor());
}
super(points, true, false, true);
this._renderer.type = "arc-segment";
for (let prop in proto13) {
Object.defineProperty(this, prop, proto13[prop]);
}
if (typeof ir === "number") {
this.innerRadius = ir;
}
if (typeof or === "number") {
this.outerRadius = or;
}
if (typeof sa === "number") {
this.startAngle = sa;
}
if (typeof ea === "number") {
this.endAngle = ea;
}
this._update();
if (typeof x === "number") {
this.translation.x = x;
}
if (typeof y === "number") {
this.translation.y = y;
}
}
/**
* @name Two.ArcSegment.Properties
* @property {String[]} - A list of properties that are on every {@link Two.ArcSegment}.
*/
static Properties = ["startAngle", "endAngle", "innerRadius", "outerRadius"];
/**
* @name Two.ArcSegment.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.ArcSegment} to create a new instance
* @returns {Two.ArcSegment}
* @description Create a new {@link Two.ArcSegment} from an object notation of a {@link Two.ArcSegment}.
* @nota-bene Works in conjunction with {@link Two.ArcSegment#toObject}
*/
static fromObject(obj) {
const segment = new _ArcSegment().copy(obj);
if ("id" in obj) {
segment.id = obj.id;
}
return segment;
}
/**
* @name Two.ArcSegment#copy
* @function
* @param {Two.ArcSegment} arcSegment - The reference {@link Two.ArcSegment}
* @description Copy the properties of one {@link Two.ArcSegment} onto another.
*/
copy(arcSegment) {
super.copy.call(this, arcSegment);
for (let i = 0; i < _ArcSegment.Properties.length; i++) {
const k = _ArcSegment.Properties[i];
if (k in arcSegment && typeof arcSegment[k] === "number") {
this[k] = arcSegment[k];
}
}
return this;
}
/**
* @name Two.ArcSegment#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagStartAngle || this._flagEndAngle || this._flagInnerRadius || this._flagOuterRadius) {
const sa = this._startAngle;
const ea = this._endAngle;
const ir = this._innerRadius;
const or = this._outerRadius;
const connected = mod(sa, TWO_PI) === mod(ea, TWO_PI);
const punctured = ir > 0;
const vertices = this.vertices;
let length = punctured ? vertices.length / 2 : vertices.length;
let command, id = 0;
let i, last, pct, v, theta, step, x, y, amp;
if (connected) {
length--;
} else if (!punctured) {
length -= 2;
}
for (i = 0, last = length - 1; i < length; i++) {
pct = i / last;
v = vertices[id];
theta = pct * (ea - sa) + sa;
step = (ea - sa) / length;
x = or * Math.cos(theta);
y = or * Math.sin(theta);
switch (i) {
case 0:
command = Commands.move;
break;
default:
command = Commands.curve;
}
v.command = command;
v.x = x;
v.y = y;
v.controls.left.clear();
v.controls.right.clear();
if (v.command === Commands.curve) {
amp = or * step / Math.PI;
v.controls.left.x = amp * Math.cos(theta - HALF_PI);
v.controls.left.y = amp * Math.sin(theta - HALF_PI);
v.controls.right.x = amp * Math.cos(theta + HALF_PI);
v.controls.right.y = amp * Math.sin(theta + HALF_PI);
if (i === 1) {
v.controls.left.multiplyScalar(2);
}
if (i === last) {
v.controls.right.multiplyScalar(2);
}
}
id++;
}
if (punctured) {
if (connected) {
vertices[id].command = Commands.close;
id++;
} else {
length--;
last = length - 1;
}
for (i = 0; i < length; i++) {
pct = i / last;
v = vertices[id];
theta = (1 - pct) * (ea - sa) + sa;
step = (ea - sa) / length;
x = ir * Math.cos(theta);
y = ir * Math.sin(theta);
command = Commands.curve;
if (i <= 0) {
command = connected ? Commands.move : Commands.line;
}
v.command = command;
v.x = x;
v.y = y;
v.controls.left.clear();
v.controls.right.clear();
if (v.command === Commands.curve) {
amp = ir * step / Math.PI;
v.controls.left.x = amp * Math.cos(theta + HALF_PI);
v.controls.left.y = amp * Math.sin(theta + HALF_PI);
v.controls.right.x = amp * Math.cos(theta - HALF_PI);
v.controls.right.y = amp * Math.sin(theta - HALF_PI);
if (i === 1) {
v.controls.left.multiplyScalar(2);
}
if (i === last) {
v.controls.right.multiplyScalar(2);
}
}
id++;
}
vertices[id].copy(vertices[0]);
vertices[id].command = Commands.line;
} else if (!connected) {
vertices[id].command = Commands.line;
vertices[id].x = 0;
vertices[id].y = 0;
id++;
vertices[id].copy(vertices[0]);
vertices[id].command = Commands.line;
}
}
super._update.call(this);
return this;
}
/**
* @name Two.ArcSegment#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
super.flagReset.call(this);
this._flagStartAngle = this._flagEndAngle = this._flagInnerRadius = this._flagOuterRadius = false;
return this;
}
/**
* @name Two.ArcSegment#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.ArcSegment}
* @description Create a new instance of {@link Two.ArcSegment} with the same properties of the current path.
*/
clone(parent) {
const ir = this.innerRadius;
const or = this.outerRadius;
const sa = this.startAngle;
const ea = this.endAngle;
const resolution = this.vertices.length;
const clone = new _ArcSegment(0, 0, ir, or, sa, ea, resolution);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.ArcSegment#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "arc-segment";
for (let i = 0; i < _ArcSegment.Properties.length; i++) {
const k = _ArcSegment.Properties[i];
object[k] = this[k];
}
return object;
}
};
var proto13 = {
startAngle: {
enumerable: true,
get: function() {
return this._startAngle;
},
set: function(v) {
this._startAngle = v;
this._flagStartAngle = true;
}
},
endAngle: {
enumerable: true,
get: function() {
return this._endAngle;
},
set: function(v) {
this._endAngle = v;
this._flagEndAngle = true;
}
},
innerRadius: {
enumerable: true,
get: function() {
return this._innerRadius;
},
set: function(v) {
this._innerRadius = v;
this._flagInnerRadius = true;
}
},
outerRadius: {
enumerable: true,
get: function() {
return this._outerRadius;
},
set: function(v) {
this._outerRadius = v;
this._flagOuterRadius = true;
}
}
};
// src/shapes/circle.js
var cos2 = Math.cos;
var sin2 = Math.sin;
var Circle = class _Circle extends Path {
/**
* @name Two.Circle#_flagRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.Circle#radius} needs updating.
*/
_flagRadius = false;
/**
* @name Two.Circle#_radius
* @private
* @see {@link Two.Circle#radius}
*/
_radius = 0;
constructor(ox, oy, r, resolution) {
const amount = resolution ? Math.max(resolution, 2) : 4;
const points = [];
for (let i = 0; i < amount; i++) {
points.push(new Anchor(0, 0, 0, 0, 0, 0));
}
super(points, true, true, true);
this._renderer.type = "circle";
for (let prop in proto14) {
Object.defineProperty(this, prop, proto14[prop]);
}
if (typeof r === "number") {
this.radius = r;
}
this._update();
if (typeof ox === "number") {
this.translation.x = ox;
}
if (typeof oy === "number") {
this.translation.y = oy;
}
}
/**
* @name Two.Circle.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Circle}.
*/
static Properties = ["radius"];
/**
* @name Two.Circle.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Circle} to create a new instance
* @returns {Two.Circle}
* @description Create a new {@link Two.Circle} from an object notation of a {@link Two.Circle}.
* @nota-bene Works in conjunction with {@link Two.Circle#toObject}
*/
static fromObject(obj) {
const circle = new _Circle().copy(obj);
if ("id" in obj) {
circle.id = obj.id;
}
return circle;
}
/**
* @name Two.Circle#copy
* @function
* @param {Two.Circle} circle - The reference {@link Two.Circle}
* @description Copy the properties of one {@link Two.Circle} onto another.
*/
copy(circle) {
super.copy.call(this, circle);
for (let i = 0; i < _Circle.Properties.length; i++) {
const k = _Circle.Properties[i];
if (k in circle && typeof circle[k] === "number") {
this[k] = circle[k];
}
}
return this;
}
/**
* @name Two.Circle#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagRadius) {
let length = this.vertices.length;
if (!this._closed && length > 2) {
length -= 1;
}
const c = 4 / 3 * Math.tan(Math.PI / (length * 2));
const radius = this._radius;
const rc = radius * c;
for (let i = 0; i < this.vertices.length; i++) {
const pct = i / length;
const theta = pct * TWO_PI;
const x = radius * cos2(theta);
const y = radius * sin2(theta);
const lx = rc * cos2(theta - HALF_PI);
const ly = rc * sin2(theta - HALF_PI);
const rx = rc * cos2(theta + HALF_PI);
const ry = rc * sin2(theta + HALF_PI);
const v = this.vertices[i];
v.command = i === 0 ? Commands.move : Commands.curve;
v.set(x, y);
v.controls.left.set(lx, ly);
v.controls.right.set(rx, ry);
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Circle#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagRadius = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Circle#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Circle}
* @description Create a new instance of {@link Two.Circle} with the same properties of the current path.
*/
clone(parent) {
const clone = new _Circle(0, 0, this.radius, this.vertices.length);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Circle#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "circle";
for (let i = 0; i < _Circle.Properties.length; i++) {
const k = _Circle.Properties[i];
object[k] = this[k];
}
return object;
}
};
var proto14 = {
radius: {
enumerable: true,
get: function() {
return this._radius;
},
set: function(v) {
this._radius = v;
this._flagRadius = true;
}
}
};
// src/shapes/ellipse.js
var cos3 = Math.cos;
var sin3 = Math.sin;
var Ellipse = class _Ellipse extends Path {
/**
* @name Two.Ellipse#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Ellipse#width} needs updating.
*/
_flagWidth = false;
/**
* @name Two.Ellipse#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Ellipse#height} needs updating.
*/
_flagHeight = false;
/**
* @name Two.Ellipse#_width
* @private
* @see {@link Two.Ellipse#width}
*/
_width = 0;
/**
* @name Two.Ellipse#_height
* @private
* @see {@link Two.Ellipse#height}
*/
_height = 0;
constructor(x, y, rx, ry, resolution) {
if (typeof ry !== "number" && typeof rx === "number") {
ry = rx;
}
const amount = resolution ? Math.max(resolution, 2) : 4;
const points = [];
for (let i = 0; i < amount; i++) {
points.push(new Anchor());
}
super(points, true, true, true);
this._renderer.type = "ellipse";
for (let prop in proto15) {
Object.defineProperty(this, prop, proto15[prop]);
}
if (typeof rx === "number") {
this.width = rx * 2;
}
if (typeof ry === "number") {
this.height = ry * 2;
}
this._update();
if (typeof x === "number") {
this.translation.x = x;
}
if (typeof y === "number") {
this.translation.y = y;
}
}
/**
* @name Two.Ellipse.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Ellipse}.
*/
static Properties = ["width", "height"];
/**
* @name Two.Ellipse.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Ellipse} to create a new instance
* @returns {Two.Ellipse}
* @description Create a new {@link Two.Ellipse} from an object notation of a {@link Two.Ellipse}.
* @nota-bene Works in conjunction with {@link Two.Ellipse#toObject}
*/
static fromObject(obj) {
const ellipse = new _Ellipse().copy(obj);
if ("id" in obj) {
ellipse.id = obj.id;
}
return ellipse;
}
/**
* @name Two.Ellipse#copy
* @function
* @param {Two.Ellipse} ellipse - The reference {@link Two.Ellipse}
* @description Copy the properties of one {@link Two.Ellipse} onto another.
*/
copy(ellipse) {
super.copy.call(this, ellipse);
for (let i = 0; i < _Ellipse.Properties.length; i++) {
const k = _Ellipse.Properties[i];
if (k in ellipse && typeof ellipse[k] === "number") {
this[k] = ellipse[k];
}
}
return this;
}
/**
* @name Two.Ellipse#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagWidth || this._flagHeight) {
let length = this.vertices.length;
if (!this._closed && length > 2) {
length -= 1;
}
const c = 4 / 3 * Math.tan(Math.PI / (this.vertices.length * 2));
const radiusX = this._width / 2;
const radiusY = this._height / 2;
for (let i = 0; i < this.vertices.length; i++) {
const pct = i / length;
const theta = pct * TWO_PI;
const x = radiusX * cos3(theta);
const y = radiusY * sin3(theta);
const lx = radiusX * c * cos3(theta - HALF_PI);
const ly = radiusY * c * sin3(theta - HALF_PI);
const rx = radiusX * c * cos3(theta + HALF_PI);
const ry = radiusY * c * sin3(theta + HALF_PI);
const v = this.vertices[i];
v.command = i === 0 ? Commands.move : Commands.curve;
v.set(x, y);
v.controls.left.set(lx, ly);
v.controls.right.set(rx, ry);
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Ellipse#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagWidth = this._flagHeight = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Ellipse#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Ellipse}
* @description Create a new instance of {@link Two.Ellipse} with the same properties of the current path.
*/
clone(parent) {
const rx = this.width / 2;
const ry = this.height / 2;
const resolution = this.vertices.length;
const clone = new _Ellipse(0, 0, rx, ry, resolution);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Ellipse#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "ellipse";
for (let i = 0; i < _Ellipse.Properties.length; i++) {
const k = _Ellipse.Properties[i];
object[k] = this[k];
}
return object;
}
};
var proto15 = {
width: {
enumerable: true,
get: function() {
return this._width;
},
set: function(v) {
this._width = v;
this._flagWidth = true;
}
},
height: {
enumerable: true,
get: function() {
return this._height;
},
set: function(v) {
this._height = v;
this._flagHeight = true;
}
}
};
// src/shapes/points.js
var ceil2 = Math.ceil;
var floor3 = Math.floor;
var Points = class _Points extends Shape {
_flagVertices = true;
_flagLength = true;
_flagFill = true;
_flagStroke = true;
_flagLinewidth = true;
_flagOpacity = true;
_flagVisible = true;
_flagSize = true;
_flagSizeAttenuation = true;
_flagStrokeAttenuation = true;
_length = 0;
_fill = "#fff";
_stroke = "#000";
_linewidth = 1;
_opacity = 1;
_visible = true;
_size = 1;
_sizeAttenuation = false;
_beginning = 0;
_ending = 1;
_dashes = null;
_strokeAttenuation = true;
constructor(vertices) {
super();
for (let prop in proto16) {
Object.defineProperty(this, prop, proto16[prop]);
}
this._renderer.type = "points";
this._renderer.flagVertices = FlagVertices.bind(this);
this._renderer.bindVertices = BindVertices.bind(this);
this._renderer.unbindVertices = UnbindVertices.bind(this);
this._renderer.flagFill = FlagFill.bind(this);
this._renderer.flagStroke = FlagStroke.bind(this);
this._renderer.vertices = null;
this._renderer.collection = null;
this.size = 1;
this.sizeAttenuation = false;
this.beginning = 0;
this.ending = 1;
this.fill = "#fff";
this.stroke = "#000";
this.linewidth = 1;
this.opacity = 1;
this.className = "";
this.visible = true;
this.vertices = vertices;
this.dashes = [];
this.dashes.offset = 0;
}
/**
* @name Two.Points.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Points}.
*/
static Properties = [
"fill",
"stroke",
"linewidth",
"opacity",
"visible",
"size",
"sizeAttenuation",
"beginning",
"ending",
"dashes",
"strokeAttenuation"
];
/**
* @name Two.Points.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Points} to create a new instance
* @returns {Two.Points}
* @description Create a new {@link Two.Points} from an object notation of a {@link Two.Points}.
* @nota-bene Works in conjunction with {@link Two.Points#toObject}
*/
static fromObject(obj) {
const fill = typeof obj.fill === "string" ? obj.fill : getEffectFromObject(obj.fill);
const stroke = typeof obj.stroke === "string" ? obj.stroke : getEffectFromObject(obj.stroke);
const points = new _Points().copy({ ...obj, fill, stroke });
if ("id" in obj) {
points.id = obj.id;
}
return points;
}
/**
* @name Two.Points#copy
* @function
* @param {Two.Points} points - The reference {@link Two.Points}
* @description Copy the properties of one {@link Two.Points} onto another.
*/
copy(points) {
super.copy.call(this, points);
for (let j = 0; j < points.vertices.length; j++) {
const v = points.vertices[j];
if (v instanceof Anchor) {
this.vertices.push(points.vertices[j].clone());
} else {
this.vertices.push(new Anchor().copy(v));
}
}
for (let i = 0; i < _Points.Properties.length; i++) {
const k = _Points.Properties[i];
if (k in points) {
this[k] = points[k];
}
}
return this;
}
/**
* @name Two.Points#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Points}
* @description Create a new instance of {@link Two.Points} with the same properties of the current path.
*/
clone(parent) {
const clone = new _Points();
for (let j = 0; j < this.vertices.length; j++) {
clone.vertices.push(this.vertices[j].clone());
}
for (let i = 0; i < _Points.Properties.length; i++) {
const k = _Points.Properties[i];
clone[k] = this[k];
}
clone.className = this.className;
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
if (parent) {
parent.add(clone);
}
return clone._update();
}
/**
* @name Two.Points#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the points object.
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = "points";
result.vertices = this.vertices.map((v) => v.toObject());
_.each(
_Points.Properties,
function(k) {
if (typeof this[k] !== "undefined") {
if (this[k].toObject) {
result[k] = this[k].toObject();
} else {
result[k] = this[k];
}
}
},
this
);
return result;
}
/**
* @name Two.Points#dispose
* @function
* @returns {Two.Points}
* @description Release the points' renderer resources and detach all events.
* This method cleans up vertices collection events, individual vertex events,
* and disposes fill/stroke effects (calling dispose() on Gradients and
* Textures for thorough cleanup) while preserving the renderer type for
* potential re-attachment to a new renderer.
*/
dispose() {
super.dispose();
if (this.vertices && typeof this.vertices.unbind === "function") {
try {
this.vertices.unbind();
} catch (e) {
}
}
if (this.vertices) {
for (let i = 0; i < this.vertices.length; i++) {
const v = this.vertices[i];
if (typeof v.unbind === "function") {
v.unbind();
}
}
}
if (typeof this.fill === "object" && typeof this.fill.dispose === "function") {
this.fill.dispose();
} else if (typeof this.fill === "object" && typeof this.fill.unbind === "function") {
this.fill.unbind();
}
if (typeof this.stroke === "object" && typeof this.stroke.dispose === "function") {
this.stroke.dispose();
} else if (typeof this.stroke === "object" && typeof this.stroke.unbind === "function") {
this.stroke.unbind();
}
return this;
}
/**
* @name Two.Points#noFill
* @function
* @description Short hand method to set fill to `none`.
*/
noFill = Path.prototype.noFill;
/**
* @name Two.Points#noStroke
* @function
* @description Short hand method to set stroke to `none`.
*/
noStroke = Path.prototype.noStroke;
/**
* @name Two.Points#corner
* @function
* @description Orient the vertices of the shape to the upper left-hand corner of the points object.
*/
corner = Path.prototype.corner;
/**
* @name Two.Points#center
* @function
* @description Orient the vertices of the shape to the center of the points object.
*/
center = Path.prototype.center;
/**
* @name Two.Points#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the path.
*/
getBoundingClientRect = Path.prototype.getBoundingClientRect;
/**
* @name Two.Points#subdivide
* @function
* @param {Number} limit - How many times to recurse subdivisions.
* @description Insert a {@link Two.Vector} at the midpoint between every item in {@link Two.Points#vertices}.
*/
subdivide(limit) {
this._update();
let points = [];
for (let i = 0; i < this.vertices.length; i++) {
const a = this.vertices[i];
const b = this.vertices[i - 1];
if (!b) {
continue;
}
const x1 = a.x;
const y1 = a.y;
const x2 = b.x;
const y2 = b.y;
const subdivisions = subdivide(x1, y1, x1, y1, x2, y2, x2, y2, limit);
points = points.concat(subdivisions);
}
this.vertices = points;
return this;
}
/**
* @name Two.Points#_updateLength
* @function
* @private
* @param {Number} [limit] -
* @param {Boolean} [silent=false] - If set to `true` then the points object isn't updated before calculation. Useful for internal use.
* @description Recalculate the {@link Two.Points#length} value.
*/
_updateLength = Path.prototype._updateLength;
/**
* @name Two.Points#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices) {
if (this._flagLength) {
this._updateLength(void 0, true);
}
const beginning = Math.min(this._beginning, this._ending);
const ending = Math.max(this._beginning, this._ending);
const bid = getIdByLength(this, beginning * this._length);
const eid = getIdByLength(this, ending * this._length);
const low = ceil2(bid);
const high = floor3(eid);
let j = 0, v;
this._renderer.vertices = [];
this._renderer.collection = [];
for (let i = 0; i < this._collection.length; i++) {
if (i >= low && i <= high) {
v = this._collection[i];
this._renderer.collection.push(v);
this._renderer.vertices[j * 2 + 0] = v.x;
this._renderer.vertices[j * 2 + 1] = v.y;
j++;
}
}
}
super._update.apply(this, arguments);
return this;
}
/**
* @name Two.Points#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagVertices = this._flagLength = this._flagFill = this._flagStroke = this._flagLinewidth = this._flagOpacity = this._flagVisible = this._flagSize = this._flagSizeAttenuation = false;
super.flagReset.call(this);
return this;
}
};
var proto16 = {
linewidth: {
enumerable: true,
get: function() {
return this._linewidth;
},
set: function(v) {
this._linewidth = v;
this._flagLinewidth = true;
}
},
opacity: {
enumerable: true,
get: function() {
return this._opacity;
},
set: function(v) {
this._opacity = v;
this._flagOpacity = true;
}
},
visible: {
enumerable: true,
get: function() {
return this._visible;
},
set: function(v) {
this._visible = v;
this._flagVisible = true;
}
},
size: {
enumerable: true,
get: function() {
return this._size;
},
set: function(v) {
this._size = v;
this._flagSize = true;
}
},
sizeAttenuation: {
enumerable: true,
get: function() {
return this._sizeAttenuation;
},
set: function(v) {
this._sizeAttenuation = v;
this._flagSizeAttenuation = true;
}
},
fill: {
enumerable: true,
get: function() {
return this._fill;
},
set: function(f) {
if (this._fill instanceof Gradient || this._fill instanceof LinearGradient || this._fill instanceof RadialGradient || this._fill instanceof Texture) {
this._fill.unbind(Events.Types.change, this._renderer.flagFill);
}
this._fill = f;
this._flagFill = true;
if (this._fill instanceof Gradient || this._fill instanceof LinearGradient || this._fill instanceof RadialGradient || this._fill instanceof Texture) {
this._fill.bind(Events.Types.change, this._renderer.flagFill);
}
}
},
stroke: {
enumerable: true,
get: function() {
return this._stroke;
},
set: function(f) {
if (this._stroke instanceof Gradient || this._stroke instanceof LinearGradient || this._stroke instanceof RadialGradient || this._stroke instanceof Texture) {
this._stroke.unbind(Events.Types.change, this._renderer.flagStroke);
}
this._stroke = f;
this._flagStroke = true;
if (this._stroke instanceof Gradient || this._stroke instanceof LinearGradient || this._stroke instanceof RadialGradient || this._stroke instanceof Texture) {
this._stroke.bind(Events.Types.change, this._renderer.flagStroke);
}
}
},
/**
* @name Two.Points#length
* @property {Number} - The sum of distances between all {@link Two.Points#vertices}.
*/
length: {
get: function() {
if (this._flagLength) {
this._updateLength();
}
return this._length;
}
},
beginning: {
enumerable: true,
get: function() {
return this._beginning;
},
set: function(v) {
this._beginning = v;
this._flagVertices = true;
}
},
ending: {
enumerable: true,
get: function() {
return this._ending;
},
set: function(v) {
this._ending = v;
this._flagVertices = true;
}
},
vertices: {
enumerable: true,
get: function() {
return this._collection;
},
set: function(vertices) {
const bindVertices = this._renderer.bindVertices;
const unbindVertices = this._renderer.unbindVertices;
if (this._collection) {
this._collection.unbind(Events.Types.insert, bindVertices).unbind(Events.Types.remove, unbindVertices);
}
if (vertices instanceof Collection) {
this._collection = vertices;
} else {
this._collection = new Collection(vertices || []);
}
this._collection.bind(Events.Types.insert, bindVertices).bind(Events.Types.remove, unbindVertices);
bindVertices(this._collection);
}
},
dashes: {
enumerable: true,
get: function() {
return this._dashes;
},
set: function(v) {
if (typeof v.offset !== "number") {
v.offset = this.dashes && this._dashes.offset || 0;
}
this._dashes = v;
}
},
/**
* @name Two.Points#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
* @description When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
*/
strokeAttenuation: {
enumerable: true,
get: function() {
return this._strokeAttenuation;
},
set: function(v) {
this._strokeAttenuation = !!v;
this._flagStrokeAttenuation = true;
this._flagLinewidth = true;
}
}
};
// src/shapes/polygon.js
var cos4 = Math.cos;
var sin4 = Math.sin;
var Polygon = class _Polygon extends Path {
/**
* @name Two.Polygon#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Polygon#width} needs updating.
*/
_flagWidth = false;
/**
* @name Two.Polygon#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Polygon#height} needs updating.
*/
_flagHeight = false;
/**
* @name Two.Polygon#_flagSides
* @private
* @property {Boolean} - Determines whether the {@link Two.Polygon#sides} needs updating.
*/
_flagSides = false;
/**
* @name Two.Polygon#_radius
* @private
* @see {@link Two.Polygon#radius}
*/
_radius = 0;
/**
* @name Two.Polygon#_width
* @private
* @see {@link Two.Polygon#width}
*/
_width = 0;
/**
* @name Two.Polygon#_height
* @private
* @see {@link Two.Polygon#height}
*/
_height = 0;
/**
* @name Two.Polygon#_sides
* @private
* @see {@link Two.Polygon#sides}
*/
_sides = 0;
constructor(x, y, radius, sides) {
sides = Math.max(sides || 0, 3);
super();
this._renderer.type = "polygon";
for (let prop in proto17) {
Object.defineProperty(this, prop, proto17[prop]);
}
this.closed = true;
this.automatic = false;
if (typeof radius === "number") {
this.radius = radius;
}
if (typeof sides === "number") {
this.sides = sides;
}
this._update();
if (typeof x === "number") {
this.translation.x = x;
}
if (typeof y === "number") {
this.translation.y = y;
}
}
/**
* @name Two.Polygon.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Polygon}.
*/
static Properties = ["width", "height", "sides"];
/**
* @name Two.Polygon.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Polygon} to create a new instance
* @returns {Two.Polygon}
* @description Create a new {@link Two.Polygon} from an object notation of a {@link Two.Polygon}.
* @nota-bene Works in conjunction with {@link Two.Polygon#toObject}
*/
static fromObject(obj) {
const polygon = new _Polygon().copy(obj);
if ("id" in obj) {
polygon.id = obj.id;
}
return polygon;
}
/**
* @name Two.Polygon#copy
* @function
* @param {Two.Polygon} polygon - The reference {@link Two.Polygon}
* @description Copy the properties of one {@link Two.Polygon} onto another.
*/
copy(polygon) {
super.copy.call(this, polygon);
for (let i = 0; i < _Polygon.Properties.length; i++) {
const k = _Polygon.Properties[i];
if (k in polygon && typeof polygon[k] === "number") {
this[k] = polygon[k];
}
}
return this;
}
/**
* @name Two.Polygon#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagWidth || this._flagHeight || this._flagSides) {
const sides = this._sides;
const amount = sides + 1;
let length = this.vertices.length;
if (length > sides) {
this.vertices.splice(sides - 1, length - sides);
length = sides;
}
for (let i = 0; i < amount; i++) {
const pct = (i + 0.5) / sides;
const theta = TWO_PI * pct + Math.PI / 2;
const x = this._width * cos4(theta) / 2;
const y = this._height * sin4(theta) / 2;
if (i >= length) {
this.vertices.push(new Anchor(x, y));
} else {
this.vertices[i].set(x, y);
}
this.vertices[i].command = i === 0 ? Commands.move : Commands.line;
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Polygon#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagWidth = this._flagHeight = this._flagSides = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Polygon#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Polygon}
* @description Create a new instance of {@link Two.Polygon} with the same properties of the current path.
*/
clone(parent) {
const clone = new _Polygon(0, 0, 0, this.sides);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
clone.width = this.width;
clone.height = this.height;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Polygon#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "polygon";
for (let i = 0; i < _Polygon.Properties.length; i++) {
const k = _Polygon.Properties[i];
object[k] = this[k];
}
return object;
}
};
var proto17 = {
radius: {
enumerable: true,
get: function() {
return this._radius;
},
set: function(v) {
this._radius = v;
this.width = v * 2;
this.height = v * 2;
}
},
width: {
enumerable: true,
get: function() {
return this._width;
},
set: function(v) {
this._width = v;
this._flagWidth = true;
this._radius = Math.max(this.width, this.height) / 2;
}
},
height: {
enumerable: true,
get: function() {
return this._height;
},
set: function(v) {
this._height = v;
this._flagHeight = true;
this._radius = Math.max(this.width, this.height) / 2;
}
},
sides: {
enumerable: true,
get: function() {
return this._sides;
},
set: function(v) {
this._sides = v;
this._flagSides = true;
}
}
};
// src/shapes/rounded-rectangle.js
var RoundedRectangle = class _RoundedRectangle extends Path {
/**
* @name Two.RoundedRectangle#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.RoundedRectangle#width} needs updating.
*/
_flagWidth = false;
/**
* @name Two.RoundedRectangle#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.RoundedRectangle#height} needs updating.
*/
_flagHeight = false;
/**
* @name Two.RoundedRectangle#_flagRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.RoundedRectangle#radius} needs updating.
*/
_flagRadius = false;
/**
* @name Two.RoundedRectangle#_width
* @private
* @see {@link Two.RoundedRectangle#width}
*/
_width = 0;
/**
* @name Two.RoundedRectangle#_height
* @private
* @see {@link Two.RoundedRectangle#height}
*/
_height = 0;
/**
* @name Two.RoundedRectangle#_radius
* @private
* @see {@link Two.RoundedRectangle#radius}
*/
_radius = 12;
constructor(x, y, width, height, radius) {
if (typeof radius === "undefined" && typeof width === "number" && typeof height === "number") {
radius = Math.floor(Math.min(width, height) / 12);
}
const points = [];
for (let i = 0; i < 10; i++) {
points.push(
new Anchor(0, 0, 0, 0, 0, 0, i === 0 ? Commands.move : Commands.curve)
);
}
super(points);
this._renderer.type = "rounded-rectangle";
for (let prop in proto18) {
Object.defineProperty(this, prop, proto18[prop]);
}
this.closed = true;
this.automatic = false;
this._renderer.flagRadius = FlagRadius.bind(this);
if (typeof width === "number") {
this.width = width;
}
if (typeof height === "number") {
this.height = height;
}
if (typeof radius === "number" || radius instanceof Vector) {
this.radius = radius;
}
this._update();
if (typeof x === "number") {
this.translation.x = x;
}
if (typeof y === "number") {
this.translation.y = y;
}
}
/**
* @name Two.RoundedRectangle.Properties
* @property {String[]} - A list of properties that are on every {@link Two.RoundedRectangle}.
*/
static Properties = ["width", "height", "radius"];
/**
* @name Two.RoundedRectangle.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.RoundedRectangle} to create a new instance
* @returns {Two.RoundedRectangle}
* @description Create a new {@link Two.RoundedRectangle} from an object notation of a {@link Two.RoundedRectangle}.
* @nota-bene Works in conjunction with {@link Two.RoundedRectangle#toObject}
*/
static fromObject(obj) {
const rectangle = new _RoundedRectangle().copy(obj);
if ("id" in obj) {
rectangle.id = obj.id;
}
return rectangle;
}
/**
* @name Two.RoundedRectangle#copy
* @function
* @param {Two.RoundedRectangle} roundedRectangle - The reference {@link Two.RoundedRectangle}
* @description Copy the properties of one {@link Two.RoundedRectangle} onto another.
*/
copy(roundedRectangle) {
super.copy.call(this, roundedRectangle);
for (let i = 0; i < _RoundedRectangle.Properties.length; i++) {
const k = _RoundedRectangle.Properties[i];
if (k in roundedRectangle) {
const value = roundedRectangle[k];
if (/radius/i.test(k)) {
this[k] = typeof value === "number" || value instanceof Vector ? value : new Vector().copy(value);
} else if (typeof value === "number") {
this[k] = value;
}
}
}
return this;
}
/**
* @name Two.RoundedRectangle#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagWidth || this._flagHeight || this._flagRadius) {
const width = this._width;
const height = this._height;
let rx, ry;
if (this._radius instanceof Vector) {
rx = this._radius.x;
ry = this._radius.y;
} else {
rx = this._radius;
ry = this._radius;
}
let v;
let w = width / 2;
let h = height / 2;
v = this.vertices[0];
v.x = -(w - rx);
v.y = -h;
v = this.vertices[1];
v.x = w - rx;
v.y = -h;
v.controls.left.clear();
v.controls.right.x = rx;
v.controls.right.y = 0;
v = this.vertices[2];
v.x = w;
v.y = -(h - ry);
v.controls.right.clear();
v.controls.left.clear();
v = this.vertices[3];
v.x = w;
v.y = h - ry;
v.controls.left.clear();
v.controls.right.x = 0;
v.controls.right.y = ry;
v = this.vertices[4];
v.x = w - rx;
v.y = h;
v.controls.right.clear();
v.controls.left.clear();
v = this.vertices[5];
v.x = -(w - rx);
v.y = h;
v.controls.left.clear();
v.controls.right.x = -rx;
v.controls.right.y = 0;
v = this.vertices[6];
v.x = -w;
v.y = h - ry;
v.controls.left.clear();
v.controls.right.clear();
v = this.vertices[7];
v.x = -w;
v.y = -(h - ry);
v.controls.left.clear();
v.controls.right.x = 0;
v.controls.right.y = -ry;
v = this.vertices[8];
v.x = -(w - rx);
v.y = -h;
v.controls.left.clear();
v.controls.right.clear();
v = this.vertices[9];
v.copy(this.vertices[8]);
}
super._update.call(this);
return this;
}
/**
* @name Two.RoundedRectangle#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagWidth = this._flagHeight = this._flagRadius = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.RoundedRectangle#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.RoundedRectangle}
* @description Create a new instance of {@link Two.RoundedRectangle} with the same properties of the current path.
*/
clone(parent) {
const width = this.width;
const height = this.height;
const radius = this.radius;
const clone = new _RoundedRectangle(0, 0, width, height, radius);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.RoundedRectangle#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "rounded-rectangle";
for (let i = 0; i < _RoundedRectangle.Properties.length; i++) {
const k = _RoundedRectangle.Properties[i];
object[k] = this[k];
}
object.radius = typeof this.radius === "number" ? this.radius : this.radius.toObject();
return object;
}
};
var proto18 = {
width: {
enumerable: true,
get: function() {
return this._width;
},
set: function(v) {
this._width = v;
this._flagWidth = true;
}
},
height: {
enumerable: true,
get: function() {
return this._height;
},
set: function(v) {
this._height = v;
this._flagHeight = true;
}
},
radius: {
enumerable: true,
get: function() {
return this._radius;
},
set: function(v) {
if (this._radius instanceof Vector) {
this._radius.unbind(Events.Types.change, this._renderer.flagRadius);
}
this._radius = v;
if (this._radius instanceof Vector) {
this._radius.bind(Events.Types.change, this._renderer.flagRadius);
}
this._flagRadius = true;
}
}
};
function FlagRadius() {
this._flagRadius = true;
}
// src/shapes/star.js
var cos5 = Math.cos;
var sin5 = Math.sin;
var Star = class _Star extends Path {
/**
* @name Two.Star#_flagInnerRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.Star#innerRadius} needs updating.
*/
_flagInnerRadius = false;
/**
* @name Two.Star#_flagOuterRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.Star#outerRadius} needs updating.
*/
_flagOuterRadius = false;
/**
* @name Two.Star#_flagSides
* @private
* @property {Boolean} - Determines whether the {@link Two.Star#sides} needs updating.
*/
_flagSides = false;
/**
* @name Two.Star#_innerRadius
* @private
* @see {@link Two.Star#innerRadius}
*/
_innerRadius = 0;
/**
* @name Two.Star#_outerRadius
* @private
* @see {@link Two.Star#outerRadius}
*/
_outerRadius = 0;
/**
* @name Two.Star#_sides
* @private
* @see {@link Two.Star#sides}
*/
_sides = 0;
constructor(x, y, innerRadius, outerRadius, sides) {
if (arguments.length <= 3) {
outerRadius = innerRadius;
innerRadius = outerRadius / 2;
}
if (typeof sides !== "number" || sides <= 0) {
sides = 5;
}
super();
this._renderer.type = "star";
for (let prop in proto19) {
Object.defineProperty(this, prop, proto19[prop]);
}
this.closed = true;
this.automatic = false;
if (typeof innerRadius === "number") {
this.innerRadius = innerRadius;
}
if (typeof outerRadius === "number") {
this.outerRadius = outerRadius;
}
if (typeof sides === "number") {
this.sides = sides;
}
this._update();
if (typeof x === "number") {
this.translation.x = x;
}
if (typeof y === "number") {
this.translation.y = y;
}
}
/**
* @name Two.Star.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Star}.
*/
static Properties = ["innerRadius", "outerRadius", "sides"];
/**
* @name Two.Star.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Star} to create a new instance
* @returns {Two.Star}
* @description Create a new {@link Two.Star} from an object notation of a {@link Two.Star}.
* @nota-bene Works in conjunction with {@link Two.Star#toObject}
*/
static fromObject(obj) {
const star = new _Star().copy(obj);
if ("id" in obj) {
star.id = obj.id;
}
return star;
}
/**
* @name Two.Star#copy
* @function
* @param {Two.Star} star - The reference {@link Two.Star}
* @description Copy the properties of one {@link Two.Star} onto another.
*/
copy(star) {
super.copy.call(this, star);
for (let i = 0; i < _Star.Properties.length; i++) {
const k = _Star.Properties[i];
if (k in star && typeof star[k] === "number") {
this[k] = star[k];
}
}
return this;
}
/**
* @name Two.Star#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagInnerRadius || this._flagOuterRadius || this._flagSides) {
const sides = this._sides * 2;
const amount = sides + 1;
let length = this.vertices.length;
if (length > sides) {
this.vertices.splice(sides - 1, length - sides);
length = sides;
}
for (let i = 0; i < amount; i++) {
const pct = (i + 0.5) / sides;
const theta = TWO_PI * pct;
const r = (!(i % 2) ? this._innerRadius : this._outerRadius) / 2;
const x = r * cos5(theta);
const y = r * sin5(theta);
if (i >= length) {
this.vertices.push(new Anchor(x, y));
} else {
this.vertices[i].set(x, y);
}
this.vertices[i].command = i === 0 ? Commands.move : Commands.line;
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Star#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagInnerRadius = this._flagOuterRadius = this._flagSides = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Star#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Star}
* @description Create a new instance of {@link Two.Star} with the same properties of the current path.
*/
clone(parent) {
const ir = this.innerRadius;
const or = this.outerRadius;
const sides = this.sides;
const clone = new _Star(0, 0, ir, or, sides);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Star#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "star";
for (let i = 0; i < _Star.Properties.length; i++) {
const k = _Star.Properties[i];
object[k] = this[k];
}
return object;
}
};
var proto19 = {
innerRadius: {
enumerable: true,
get: function() {
return this._innerRadius;
},
set: function(v) {
this._innerRadius = v;
this._flagInnerRadius = true;
}
},
outerRadius: {
enumerable: true,
get: function() {
return this._outerRadius;
},
set: function(v) {
this._outerRadius = v;
this._flagOuterRadius = true;
}
},
sides: {
enumerable: true,
get: function() {
return this._sides;
},
set: function(v) {
this._sides = v;
this._flagSides = true;
}
}
};
// src/text.js
var canvas;
var min2 = Math.min;
var max2 = Math.max;
if (root.document) {
canvas = document.createElement("canvas");
}
var Text = class _Text extends Shape {
/**
* @name Two.Text#_flagValue
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#value} need updating.
*/
_flagValue = true;
/**
* @name Two.Text#_flagFamily
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#family} need updating.
*/
_flagFamily = true;
/**
* @name Two.Text#_flagSize
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#size} need updating.
*/
_flagSize = true;
/**
* @name Two.Text#_flagLeading
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#leading} need updating.
*/
_flagLeading = true;
/**
* @name Two.Text#_flagAlignment
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#alignment} need updating.
*/
_flagAlignment = true;
/**
* @name Two.Text#_flagBaseline
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#baseline} need updating.
*/
_flagBaseline = true;
/**
* @name Two.Text#_flagStyle
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#style} need updating.
*/
_flagStyle = true;
/**
* @name Two.Text#_flagWeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#weight} need updating.
*/
_flagWeight = true;
/**
* @name Two.Text#_flagDecoration
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#decoration} need updating.
*/
_flagDecoration = true;
/**
* @name Two.Text#_flagFill
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#fill} need updating.
*/
_flagFill = true;
/**
* @name Two.Text#_flagStroke
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#stroke} need updating.
*/
_flagStroke = true;
/**
* @name Two.Text#_flagLinewidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#linewidth} need updating.
*/
_flagLinewidth = true;
/**
* @name Two.Text#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#opacity} need updating.
*/
_flagOpacity = true;
/**
* @name Two.Text#_flagVisible
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#visible} need updating.
*/
_flagVisible = true;
/**
* @name Two.Text#_flagMask
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#mask} needs updating.
*/
_flagMask = false;
/**
* @name Two.Text#_flagClip
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#clip} needs updating.
*/
_flagClip = false;
/**
* @name Two.Text#_flagDirection
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#direction} needs updating.
*/
_flagDirection = true;
/**
* @name Two.Text#_flagStrokeAttenuation
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#strokeAttenuation} needs updating.
*/
_flagStrokeAttenuation = true;
// Underlying Properties
/**
* @name Two.Text#value
* @property {String} - The characters to be rendered to the the screen. Referred to in the documentation sometimes as the `message`.
*/
_value = "";
/**
* @name Two.Text#family
* @property {String} - The font family Two.js should attempt to register for rendering. The default value is `'sans-serif'`. Comma separated font names can be supplied as a "stack", similar to the CSS implementation of `font-family`.
*/
_family = "sans-serif";
/**
* @name Two.Text#size
* @property {Number} - The font size in Two.js point space. Defaults to `13`.
*/
_size = 13;
/**
* @name Two.Text#leading
* @property {Number} - The height between lines measured from base to base in Two.js point space. Defaults to `17`.
*/
_leading = 17;
/**
* @name Two.Text#alignment
* @property {String} - Alignment of text in relation to {@link Two.Text#translation}'s coordinates. Possible values include `'left'`, `'center'`, `'right'`. Defaults to `'center'`.
*/
_alignment = "center";
/**
* @name Two.Text#baseline
* @property {String} - The vertical aligment of the text in relation to {@link Two.Text#translation}'s coordinates. Possible values include `'top'`, `'middle'`, `'bottom'`, and `'baseline'`. Defaults to `'baseline'`.
* @nota-bene In headless environments where the canvas is based on {@link https://github.com/Automattic/node-canvas}, `baseline` seems to be the only valid property.
*/
_baseline = "middle";
/**
* @name Two.Text#style
* @property {String} - The font's style. Possible values include '`normal`', `'italic'`. Defaults to `'normal'`.
*/
_style = "normal";
/**
* @name Two.Text#weight
* @property {Number} - A number at intervals of 100 to describe the font's weight. This compatibility varies with the typeface's variant weights. Larger values are bolder. Smaller values are thinner. Defaults to `'500'`.
*/
_weight = 500;
/**
* @name Two.Text#decoration
* @property {String} - String to delineate whether text should be decorated with for instance an `'underline'`. Defaults to `'none'`.
*/
_decoration = "none";
/**
* @name Two.Text#direction
* @property {String} - String to determine what direction the text should run. Possibly values are `'ltr'` for left-to-right and `'rtl'` for right-to-left. Defaults to `'ltr'`.
*/
_direction = "ltr";
/**
* @name Two.Text#fill
* @property {(String|Two.Gradient|Two.Texture)} - The value of what the text object should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
_fill = "#000";
/**
* @name Two.Text#stroke
* @property {(String|Two.Gradient|Two.Texture)} - The value of what the text object should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
_stroke = "none";
/**
* @name Two.Text#linewidth
* @property {Number} - The thickness in pixels of the stroke.
*/
_linewidth = 1;
/**
* @name Two.Text#opacity
* @property {Number} - The opaqueness of the text object.
* @nota-bene Can be used in conjunction with CSS Colors that have an alpha value.
*/
_opacity = 1;
/**
* @name Two.Text#visible
* @property {Boolean} - Display the text object or not.
* @nota-bene For {@link Two.CanvasRenderer} and {@link Two.WebGLRenderer} when set to false all updating is disabled improving performance dramatically with many objects in the scene.
*/
_visible = true;
/**
* @name Two.Text#mask
* @property {Two.Shape} - The shape whose alpha property becomes a clipping area for the text.
* @nota-bene This property is currently not working because of SVG spec issues found here {@link https://code.google.com/p/chromium/issues/detail?id=370951}.
*/
_mask = null;
/**
* @name Two.Text#clip
* @property {Two.Shape} - Object to define clipping area.
* @nota-bene This property is currently not working because of SVG spec issues found here {@link https://code.google.com/p/chromium/issues/detail?id=370951}.
*/
_clip = false;
/**
* @name Two.Text#_dashes
* @private
* @see {@link Two.Text#dashes}
*/
_dashes = null;
/**
* @name Two.Text#_strokeAttenuation
* @private
* @see {@link Two.Text#strokeAttenuation}
*/
_strokeAttenuation = true;
constructor(message, x, y, styles) {
super();
for (let prop in proto20) {
Object.defineProperty(this, prop, proto20[prop]);
}
this._renderer.type = "text";
this._renderer.flagFill = FlagFill2.bind(this);
this._renderer.flagStroke = FlagStroke2.bind(this);
this.value = message;
if (typeof x === "number") {
this.translation.x = x;
}
if (typeof y === "number") {
this.translation.y = y;
}
this.dashes = [];
this.dashes.offset = 0;
if (!_.isObject(styles)) {
return this;
}
for (let i = 0; i < _Text.Properties.length; i++) {
const property = _Text.Properties[i];
if (property in styles) {
this[property] = styles[property];
}
}
}
/**
* @name Two.Text.Ratio
* @property {Number} - Approximate aspect ratio of a typeface's character width to height.
*/
static Ratio = 0.6;
/**
* @name Two.Text.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Text}.
*/
static Properties = [
"value",
"family",
"size",
"leading",
"alignment",
"linewidth",
"style",
"weight",
"decoration",
"direction",
"baseline",
"opacity",
"visible",
"fill",
"stroke",
"dashes",
"strokeAttenuation"
];
/**
*
* @name Two.Measure
* @function
* @param {Two.Text} [text] - The instance of {@link Two.Text} to measure.
* @returns {Object} - The width and height of the {@link Two.Text} instance.
*/
static Measure(text) {
if (canvas) {
const ctx = canvas.getContext("2d");
ctx.font = [
text._style,
text._weight,
`${text._size}px/${text._leading}px`,
text._family
].join(" ");
const metrics = ctx.measureText(text.value, 0, 0);
const height = metrics.actualBoundingBoxDescent + metrics.actualBoundingBoxAscent;
return {
width: metrics.width,
height
};
} else {
const width = this.value.length * this.size * _Text.Ratio;
const height = this.leading;
console.warn(
"Two.Text: unable to accurately measure text, so using an approximation."
);
return {
width,
height
};
}
}
/**
* @name Two.Text.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Text} to create a new instance
* @returns {Two.Text}
* @description Create a new {@link Two.Text} from an object notation of a {@link Two.Text}.
* @nota-bene Works in conjunction with {@link Two.Text#toObject}
*/
static fromObject(obj) {
const fill = typeof obj.fill === "string" ? obj.fill : getEffectFromObject(obj.fill);
const stroke = typeof obj.stroke === "string" ? obj.stroke : getEffectFromObject(obj.stroke);
const text = new _Text().copy({ ...obj, fill, stroke });
if ("id" in obj) {
text.id = obj.id;
}
return text;
}
/**
* @name Two.Text#copy
* @function
* @param {Two.Text} text
* @description Copy the properties of one {@link Two.Text} onto another.
*/
copy(text) {
super.copy.call(this, text);
for (let i = 0; i < _Text.Properties.length; i++) {
const k = _Text.Properties[i];
if (k in text) {
this[k] = text[k];
}
}
return this;
}
/**
* @name Two.Text#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Text}
* @description Create a new instance of {@link Two.Text} with the same properties of the current text object.
*/
clone(parent) {
const clone = new _Text(this.value);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
for (let i = 0; i < _Text.Properties.length; i++) {
const prop = _Text.Properties[i];
clone[prop] = this[prop];
}
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
if (parent) {
parent.add(clone);
}
return clone._update();
}
/**
* @name Two.Text#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the text object.
* @nota-bene Works in conjunction with {@link Two.Text.fromObject}
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = "text";
for (let i = 0; i < _Text.Properties.length; i++) {
const prop = _Text.Properties[i];
result[prop] = this[prop];
}
return result;
}
/**
* @name Two.Text#dispose
* @function
* @returns {Two.Text}
* @description Release the text's renderer resources and detach all events.
* This method disposes fill and stroke effects (calling dispose() on
* Gradients and Textures for thorough cleanup) while preserving the
* renderer type for potential re-attachment to a new renderer.
*/
dispose() {
super.dispose();
if (typeof this.fill === "object" && typeof this.fill.dispose === "function") {
this.fill.dispose();
} else if (typeof this.fill === "object" && typeof this.fill.unbind === "function") {
this.fill.unbind();
}
if (typeof this.stroke === "object" && typeof this.stroke.dispose === "function") {
this.stroke.dispose();
} else if (typeof this.stroke === "object" && typeof this.stroke.unbind === "function") {
this.stroke.unbind();
}
return this;
}
/**
* @name Two.Text#noFill
* @function
* @description Short hand method to set fill to `none`.
*/
noFill() {
this.fill = "none";
return this;
}
/**
* @name Two.Text#noStroke
* @function
* @description Short hand method to set stroke to `none`.
*/
noStroke() {
this.stroke = "none";
this.linewidth = 0;
return this;
}
// A shim to not break `getBoundingClientRect` calls.
// TODO: Implement a way to calculate proper bounding
// boxes of `Two.Text`.
/**
* @name Two.Text#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the text object.
*/
getBoundingClientRect(shallow) {
let matrix;
let left, right, top, bottom;
this._update(true);
matrix = shallow ? this.matrix : this.worldMatrix;
const { width, height } = _Text.Measure(this);
const border = (this._linewidth || 0) / 2;
switch (this.alignment) {
case "left":
left = -border;
right = width + border;
break;
case "right":
left = -(width + border);
right = border;
break;
default:
left = -(width / 2 + border);
right = width / 2 + border;
}
switch (this.baseline) {
case "middle":
top = -(height / 2 + border);
bottom = height / 2 + border;
break;
default:
top = -(height + border);
bottom = border;
}
const [ax, ay] = matrix.multiply(left, top);
const [bx, by] = matrix.multiply(left, bottom);
const [cx, cy] = matrix.multiply(right, top);
const [dx, dy] = matrix.multiply(right, bottom);
top = min2(ay, by, cy, dy);
left = min2(ax, bx, cx, dx);
right = max2(ax, bx, cx, dx);
bottom = max2(ay, by, cy, dy);
return {
top,
left,
right,
bottom,
width: right - left,
height: bottom - top
};
}
/**
* @name Two.Text#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
super.flagReset.call(this);
this._flagValue = this._flagFamily = this._flagSize = this._flagLeading = this._flagAlignment = this._flagFill = this._flagStroke = this._flagLinewidth = this._flagOpacity = this._flagVisible = this._flagClip = this._flagDecoration = this._flagClassName = this._flagBaseline = this._flagWeight = this._flagStyle = this._flagDirection = false;
return this;
}
};
var proto20 = {
value: {
enumerable: true,
get: function() {
return this._value;
},
set: function(v) {
this._value = v;
this._flagValue = true;
}
},
family: {
enumerable: true,
get: function() {
return this._family;
},
set: function(v) {
this._family = v;
this._flagFamily = true;
}
},
size: {
enumerable: true,
get: function() {
return this._size;
},
set: function(v) {
this._size = v;
this._flagSize = true;
}
},
leading: {
enumerable: true,
get: function() {
return this._leading;
},
set: function(v) {
this._leading = v;
this._flagLeading = true;
}
},
alignment: {
enumerable: true,
get: function() {
return this._alignment;
},
set: function(v) {
this._alignment = v;
this._flagAlignment = true;
}
},
linewidth: {
enumerable: true,
get: function() {
return this._linewidth;
},
set: function(v) {
this._linewidth = v;
this._flagLinewidth = true;
}
},
style: {
enumerable: true,
get: function() {
return this._style;
},
set: function(v) {
this._style = v;
this._flagStyle = true;
}
},
weight: {
enumerable: true,
get: function() {
return this._weight;
},
set: function(v) {
this._weight = v;
this._flagWeight = true;
}
},
decoration: {
enumerable: true,
get: function() {
return this._decoration;
},
set: function(v) {
this._decoration = v;
this._flagDecoration = true;
}
},
direction: {
enumerable: true,
get: function() {
return this._direction;
},
set: function(v) {
this._direction = v;
this._flagDirection = true;
}
},
baseline: {
enumerable: true,
get: function() {
return this._baseline;
},
set: function(v) {
this._baseline = v;
this._flagBaseline = true;
}
},
opacity: {
enumerable: true,
get: function() {
return this._opacity;
},
set: function(v) {
this._opacity = v;
this._flagOpacity = true;
}
},
visible: {
enumerable: true,
get: function() {
return this._visible;
},
set: function(v) {
this._visible = v;
this._flagVisible = true;
}
},
fill: {
enumerable: true,
get: function() {
return this._fill;
},
set: function(f) {
if (this._fill instanceof Gradient || this._fill instanceof LinearGradient || this._fill instanceof RadialGradient || this._fill instanceof Texture) {
this._fill.unbind(Events.Types.change, this._renderer.flagFill);
}
this._fill = f;
this._flagFill = true;
if (this._fill instanceof Gradient || this._fill instanceof LinearGradient || this._fill instanceof RadialGradient || this._fill instanceof Texture) {
this._fill.bind(Events.Types.change, this._renderer.flagFill);
}
}
},
stroke: {
enumerable: true,
get: function() {
return this._stroke;
},
set: function(f) {
if (this._stroke instanceof Gradient || this._stroke instanceof LinearGradient || this._stroke instanceof RadialGradient || this._stroke instanceof Texture) {
this._stroke.unbind(Events.Types.change, this._renderer.flagStroke);
}
this._stroke = f;
this._flagStroke = true;
if (this._stroke instanceof Gradient || this._stroke instanceof LinearGradient || this._stroke instanceof RadialGradient || this._stroke instanceof Texture) {
this._stroke.bind(Events.Types.change, this._renderer.flagStroke);
}
}
},
mask: {
enumerable: true,
get: function() {
return this._mask;
},
set: function(v) {
this._mask = v;
this._flagMask = true;
if (_.isObject(v) && !v.clip) {
v.clip = true;
}
}
},
clip: {
enumerable: true,
get: function() {
return this._clip;
},
set: function(v) {
this._clip = v;
this._flagClip = true;
}
},
dashes: {
enumerable: true,
get: function() {
return this._dashes;
},
set: function(v) {
if (typeof v.offset !== "number") {
v.offset = this.dashes && this._dashes.offset || 0;
}
this._dashes = v;
}
},
/**
* @name Two.Text#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
* @description When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
*/
strokeAttenuation: {
enumerable: true,
get: function() {
return this._strokeAttenuation;
},
set: function(v) {
this._strokeAttenuation = !!v;
this._flagStrokeAttenuation = true;
this._flagLinewidth = true;
}
}
};
function FlagFill2() {
this._flagFill = true;
}
function FlagStroke2() {
this._flagStroke = true;
}
// src/effects/image-sequence.js
var ImageSequence = class _ImageSequence extends Rectangle {
/**
* @name Two.ImageSequence#_flagTextures
* @private
* @property {Boolean} - Determines whether the {@link Two.ImageSequence#textures} need updating.
*/
_flagTextures = false;
/**
* @name Two.ImageSequence#_flagFrameRate
* @private
* @property {Boolean} - Determines whether the {@link Two.ImageSequence#frameRate} needs updating.
*/
_flagFrameRate = false;
/**
* @name Two.ImageSequence#_flagIndex
* @private
* @property {Boolean} - Determines whether the {@link Two.ImageSequence#index} needs updating.
*/
_flagIndex = false;
// Private variables
/**
* @name Two.ImageSequence#_amount
* @private
* @property {Number} - Number of frames for a given {@link Two.ImageSequence}.
*/
_amount = 1;
/**
* @name Two.ImageSequence#_duration
* @private
* @property {Number} - Number of milliseconds a {@link Two.ImageSequence}.
*/
_duration = 0;
/**
* @name Two.ImageSequence#_index
* @private
* @property {Number} - The current frame the {@link Two.ImageSequence} is currently displaying.
*/
_index = 0;
/**
* @name Two.ImageSequence#_startTime
* @private
* @property {Milliseconds} - Epoch time in milliseconds of when the {@link Two.ImageSequence} started.
*/
_startTime = 0;
/**
* @name Two.ImageSequence#_playing
* @private
* @property {Boolean} - Dictates whether the {@link Two.ImageSequence} is animating or not.
*/
_playing = false;
/**
* @name Two.ImageSequence#_firstFrame
* @private
* @property {Number} - The frame the {@link Two.ImageSequence} should start with.
*/
_firstFrame = 0;
/**
* @name Two.ImageSequence#_lastFrame
* @private
* @property {Number} - The frame the {@link Two.ImageSequence} should end with.
*/
_lastFrame = 0;
/**
* @name Two.ImageSequence#_playing
* @private
* @property {Boolean} - Dictates whether the {@link Two.ImageSequence} should loop or not.
*/
_loop = true;
// Exposed through getter-setter
/**
* @name Two.ImageSequence#_textures
* @private
* @see {@link Two.ImageSequence#textures}
*/
_textures = null;
/**
* @name Two.ImageSequence#_frameRate
* @private
* @see {@link Two.ImageSequence#frameRate}
*/
_frameRate = 0;
/**
* @name Two.ImageSequence#_origin
* @private
* @see {@link Two.ImageSequence#origin}
*/
_origin = null;
constructor(src, ox, oy, frameRate) {
super(ox, oy, 0, 0);
this._renderer.type = "image-sequence";
for (let prop in proto21) {
Object.defineProperty(this, prop, proto21[prop]);
}
this._renderer.flagTextures = FlagTextures.bind(this);
this._renderer.bindTextures = BindTextures.bind(this);
this._renderer.unbindTextures = UnbindTextures.bind(this);
this.noStroke();
this.noFill();
if (Array.isArray(src)) {
this.textures = src.map(GenerateTexture.bind(this));
} else if (typeof src === "string") {
this.textures = [GenerateTexture(src)];
}
this.origin = new Vector();
this._update();
if (typeof frameRate === "number") {
this.frameRate = frameRate;
} else {
this.frameRate = _ImageSequence.DefaultFrameRate;
}
this.index = 0;
}
/**
* @name Two.ImageSequence.Properties
* @property {String[]} - A list of properties that are on every {@link Two.ImageSequence}.
*/
static Properties = [
"textures",
"frameRate",
"index",
"firstFrame",
"lastFrame",
"loop"
];
/**
* @name Two.ImageSequence.DefaultFrameRate
* @property The default frame rate that {@link Two.ImageSequence#frameRate} is set to when instantiated.
*/
static DefaultFrameRate = 30;
/**
* @name Two.ImageSequence.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.ImageSequence} to create a new instance
* @returns {Two.ImageSequence}
* @description Create a new {@link Two.ImageSequence} from an object notation of a {@link Two.ImageSequence}.
* @nota-bene Works in conjunction with {@link Two.ImageSequence#toObject}
*/
static fromObject(obj) {
const sequence = new _ImageSequence().copy(obj);
if ("id" in obj) {
sequence.id = obj.id;
}
return sequence;
}
/**
* @name Two.ImageSequence#copy
* @function
* @param {Two.ImageSequence} imageSequence - The reference {@link Two.ImageSequence}
* @description Copy the properties of one {@link Two.ImageSequence} onto another.
*/
copy(imageSequence) {
super.copy.call(this, imageSequence);
for (let i = 0; i < _ImageSequence.Properties.length; i++) {
const k = _ImageSequence.Properties[i];
if (k in imageSequence) {
this[k] = imageSequence[k];
}
}
return this;
}
/**
* @name Two.ImageSequence#play
* @function
* @param {Number} [firstFrame=0] - The index of the frame to start the animation with.
* @param {Number} [lastFrame] - The index of the frame to end the animation with. Defaults to the last item in the {@link Two.ImageSequence#textures}.
* @param {Function} [onLastFrame] - Optional callback function to be triggered after playing the last frame. This fires multiple times when the image sequence is looped.
* @description Initiate animation playback of a {@link Two.ImageSequence}.
*/
play(firstFrame, lastFrame, onLastFrame) {
this._playing = true;
this._firstFrame = 0;
this._lastFrame = this.amount - 1;
this._startTime = _.performance.now();
if (typeof firstFrame === "number") {
this._firstFrame = firstFrame;
}
if (typeof lastFrame === "number") {
this._lastFrame = lastFrame;
}
if (typeof onLastFrame === "function") {
this._onLastFrame = onLastFrame;
} else {
delete this._onLastFrame;
}
if (this._index !== this._firstFrame) {
this._startTime -= 1e3 * Math.abs(this._index - this._firstFrame) / this._frameRate;
}
return this;
}
/**
* @name Two.ImageSequence#pause
* @function
* @description Halt animation playback of a {@link Two.ImageSequence}.
*/
pause() {
this._playing = false;
return this;
}
/**
* @name Two.ImageSequence#stop
* @function
* @description Halt animation playback of a {@link Two.ImageSequence} and set the current frame back to the first frame.
*/
stop() {
this._playing = false;
this._index = this._firstFrame;
return this;
}
/**
* @name Two.ImageSequence#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.ImageSequence}
* @description Create a new instance of {@link Two.ImageSequence} with the same properties of the current image sequence.
*/
clone(parent) {
const clone = new _ImageSequence(
this.textures,
this.translation.x,
this.translation.y,
this.frameRate
);
clone._loop = this._loop;
if (this._playing) {
clone.play();
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.ImageSequence#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "image-sequence";
object.textures = this.textures.map(function(texture) {
return texture.toObject();
});
object.frameRate = this.frameRate;
object.index = this.index;
object.firstFrame = this.firstFrame;
object.lastFrame = this.lastFrame;
object.loop = this.loop;
return object;
}
/**
* @name Two.ImageSequence#dispose
* @function
* @returns {Two.ImageSequence}
* @description Release the image sequence's renderer resources and detach all events.
* This method stops any running animation, clears animation callbacks, unbinds
* textures collection events, and disposes individual textures (calling dispose()
* for thorough cleanup) while preserving the renderer type for potential
* re-attachment to a new renderer.
*/
dispose() {
super.dispose();
if (this._playing) {
this._playing = false;
}
this._onLastFrame = null;
if (this.textures && typeof this.textures.unbind === "function") {
try {
this.textures.unbind();
} catch (e) {
}
}
if (this.textures) {
for (let i = 0; i < this.textures.length; i++) {
const texture = this.textures[i];
if (typeof texture.dispose === "function") {
texture.dispose();
} else if (typeof texture.unbind === "function") {
texture.unbind();
}
}
}
return this;
}
/**
* @name Two.ImageSequence#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
const effect = this._textures;
let width, height, elapsed, amount, duration, texture;
let index, frames;
if (effect) {
if (this._flagTextures) {
this._amount = effect.length;
}
if (this._flagFrameRate) {
this._duration = 1e3 * this._amount / this._frameRate;
}
if (this._playing && this._frameRate > 0) {
amount = this._amount;
if (_.isNaN(this._lastFrame)) {
this._lastFrame = amount - 1;
}
elapsed = _.performance.now() - this._startTime;
frames = this._lastFrame + 1;
duration = 1e3 * (frames - this._firstFrame) / this._frameRate;
if (this._loop) {
elapsed = elapsed % duration;
} else {
elapsed = Math.min(elapsed, duration);
}
index = lerp(this._firstFrame, frames, elapsed / duration);
index = Math.floor(index);
if (index !== this._index) {
this._index = index;
texture = effect[this._index];
if (texture.loaded) {
width = texture.image.width;
height = texture.image.height;
if (this.width !== width) {
this.width = width;
}
if (this.height !== height) {
this.height = height;
}
this.fill = texture;
if (index >= this._lastFrame - 1 && this._onLastFrame) {
this._onLastFrame();
}
}
}
} else if (this._flagIndex || !(this.fill instanceof Texture)) {
texture = effect[this._index];
if (texture.loaded) {
width = texture.image.width;
height = texture.image.height;
if (this.width !== width) {
this.width = width;
}
if (this.height !== height) {
this.height = height;
}
}
this.fill = texture;
}
}
super._update.call(this);
return this;
}
/**
* @name Two.ImageSequence#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagTextures = this._flagFrameRate = false;
super.flagReset.call(this);
return this;
}
};
var proto21 = {
frameRate: {
enumerable: true,
get: function() {
return this._frameRate;
},
set: function(v) {
this._frameRate = v;
this._flagFrameRate = true;
}
},
index: {
enumerable: true,
get: function() {
return this._index;
},
set: function(v) {
this._index = v;
this._flagIndex = true;
}
},
textures: {
enumerable: true,
get: function() {
return this._textures;
},
set: function(textures) {
const bindTextures = this._renderer.bindTextures;
const unbindTextures = this._renderer.unbindTextures;
if (this._textures) {
this._textures.unbind(Events.Types.insert, bindTextures).unbind(Events.Types.remove, unbindTextures);
}
this._textures = new Collection((textures || []).slice(0));
this._textures.bind(Events.Types.insert, bindTextures).bind(Events.Types.remove, unbindTextures);
bindTextures(this._textures);
}
},
firstFrame: {
enumerable: true,
get: function() {
return this._firstFrame;
},
set: function(v) {
this._firstFrame = v;
}
},
lastFrame: {
enumerable: true,
get: function() {
return this._lastFrame;
},
set: function(v) {
this._lastFrame = v;
}
},
loop: {
enumerable: true,
get: function() {
return this._loop;
},
set: function(v) {
this._loop = !!v;
}
}
};
function FlagTextures() {
this._flagTextures = true;
}
function BindTextures(items) {
let i = items.length;
while (i--) {
items[i].bind(Events.Types.change, this._renderer.flagTextures);
}
this._renderer.flagTextures();
}
function UnbindTextures(items) {
let i = items.length;
while (i--) {
items[i].unbind(Events.Types.change, this._renderer.flagTextures);
}
this._renderer.flagTextures();
}
function GenerateTexture(obj) {
if (obj instanceof Texture) {
return obj;
} else if (typeof obj === "string") {
return new Texture(obj);
}
}
// src/group.js
var min3 = Math.min;
var max3 = Math.max;
var cache = {
getShapesAtPoint: {
results: [],
hitOptions: {},
context: {
x: 0,
y: 0,
visibleOnly: true,
results: null
},
single: [],
output: [],
empty: []
}
};
var Group = class _Group extends Shape {
/**
* @name Two.Group#_flagAdditions
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#additions} needs updating.
*/
_flagAdditions = false;
/**
* @name Two.Group#_flagSubtractions
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#subtractions} needs updating.
*/
_flagSubtractions = false;
/**
* @name Two.Group#_flagOrder
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#order} needs updating.
*/
_flagOrder = false;
/**
* @name Two.Group#_flagVisible
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#visible} needs updating.
*/
/**
* @name Two.Group#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#opacity} needs updating.
*/
_flagOpacity = true;
/**
* @name Two.Group#_flagBeginning
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#beginning} needs updating.
*/
_flagBeginning = false;
/**
* @name Two.Group#_flagEnding
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#ending} needs updating.
*/
_flagEnding = false;
/**
* @name Two.Group#_flagLength
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#length} needs updating.
*/
_flagLength = false;
/**
* @name Two.Group#_flagMask
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#mask} needs updating.
*/
_flagMask = false;
// Underlying Properties
/**
* @name Two.Group#fill
* @property {(String|Two.Gradient|Two.Texture)} - The value of what all child shapes should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
_fill = "#fff";
/**
* @name Two.Group#stroke
* @property {(String|Two.Gradient|Two.Texture)} - The value of what all child shapes should be outlined in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
_stroke = "#000";
/**
* @name Two.Group#linewidth
* @property {Number} - The thickness in pixels of the stroke for all child shapes.
*/
_linewidth = 1;
/**
* @name Two.Group#opacity
* @property {Number} - The opaqueness of all child shapes.
* @nota-bene Becomes multiplied by the individual child's opacity property.
*/
_opacity = 1;
/**
* @name Two.Group#visible
* @property {Boolean} - Display the path or not.
* @nota-bene For {@link Two.CanvasRenderer} and {@link Two.WebGLRenderer} when set to false all updating is disabled improving performance dramatically with many objects in the scene.
*/
_visible = true;
/**
* @name Two.Group#cap
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeLinecapProperty}
*/
_cap = "round";
/**
* @name Two.Group#join
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeLinejoinProperty}
*/
_join = "round";
/**
* @name Two.Group#miter
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeMiterlimitProperty}
*/
_miter = 4;
/**
* @name Two.Group#closed
* @property {Boolean} - Determines whether a final line is drawn between the final point in the `vertices` array and the first point of all child shapes.
*/
_closed = true;
/**
* @name Two.Group#curved
* @property {Boolean} - When the child's path is `automatic = true` this boolean determines whether the lines between the points are curved or not.
*/
_curved = false;
/**
* @name Two.Group#automatic
* @property {Boolean} - Determines whether or not Two.js should calculate curves, lines, and commands automatically for you or to let the developer manipulate them for themselves.
*/
_automatic = true;
/**
* @name Two.Group#beginning
* @property {Number} - Number between zero and one to state the beginning of where the path is rendered.
* @description {@link Two.Group#beginning} is a percentage value that represents at what percentage into all child shapes should the renderer start drawing.
* @nota-bene This is great for animating in and out stroked paths in conjunction with {@link Two.Group#ending}.
*/
_beginning = 0;
/**
* @name Two.Group#ending
* @property {Number} - Number between zero and one to state the ending of where the path is rendered.
* @description {@link Two.Group#ending} is a percentage value that represents at what percentage into all child shapes should the renderer start drawing.
* @nota-bene This is great for animating in and out stroked paths in conjunction with {@link Two.Group#beginning}.
*/
_ending = 1;
/**
* @name Two.Group#length
* @property {Number} - The sum of distances between all child lengths.
*/
_length = 0;
/**
* @name Two.Group#mask
* @property {Two.Shape} - The Two.js object to clip from a group's rendering.
*/
_mask = null;
/**
* @name Two.Group#_strokeAttenuation
* @private
* @see {@link Two.Group#strokeAttenuation}
*/
_strokeAttenuation = true;
constructor(children) {
super();
for (let prop in proto22) {
Object.defineProperty(this, prop, proto22[prop]);
}
this._renderer.type = "group";
this.additions = [];
this.subtractions = [];
this.children = Array.isArray(children) ? children : Array.prototype.slice.call(arguments);
}
static Children = Children;
/**
* @name Two.Group.InsertChildren
* @function
* @param {Two.Shape[]} children - The objects to be inserted.
* @description Cached method to let renderers know children have been added to a {@link Two.Group}.
*/
static InsertChildren(children) {
for (let i = 0; i < children.length; i++) {
replaceParent.call(this, children[i], this);
}
}
/**
* @name Two.Group.RemoveChildren
* @function
* @param {Two.Shape[]} children - The objects to be removed.
* @description Cached method to let renderers know children have been removed from a {@link Two.Group}.
*/
static RemoveChildren(children) {
for (let i = 0; i < children.length; i++) {
replaceParent.call(this, children[i]);
}
}
/**
* @name Two.Group.OrderChildren
* @function
* @description Cached method to let renderers know order has been updated on a {@link Two.Group}.
*/
static OrderChildren(children) {
this._flagOrder = true;
}
/**
* @name Two.Group.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Group}.
*/
static Properties = [
"fill",
"stroke",
"linewidth",
"cap",
"join",
"miter",
"closed",
"curved",
"automatic"
];
static fromObject(obj) {
const group = new _Group();
for (let i = 0; i < _Group.Properties.length; i++) {
const k = _Group.Properties[i];
if (k in obj) {
if (/(fill|stroke)/i.test(k)) {
group[k] = typeof obj[k] === "string" ? obj[k] : getEffectFromObject(obj[k]);
} else {
group[k] = obj[k];
}
}
}
if ("mask" in obj) {
group.mask = getShapeFromObject(obj.mask);
}
if ("id" in obj) {
group.id = obj.id;
}
group.children = obj.children.map(getShapeFromObject);
return group;
function getShapeFromObject(child) {
if (child && child.renderer) {
switch (child.renderer.type) {
case "arc-segment":
return ArcSegment.fromObject(child);
case "circle":
return Circle.fromObject(child);
case "element":
return Element.fromObject(child);
case "ellipse":
return Ellipse.fromObject(child);
case "group":
return _Group.fromObject(child);
case "image":
return Image.fromObject(child);
case "image-sequence":
return ImageSequence.fromObject(child);
case "path":
return Path.fromObject(child);
case "points":
return Points.fromObject(child);
case "polygon":
return Polygon.fromObject(child);
case "rectangle":
return Rectangle.fromObject(child);
case "rounded-rectangle":
return RoundedRectangle.fromObject(child);
case "shape":
return Shape.fromObject(child);
case "sprite":
return Sprite.fromObject(child);
case "star":
return Star.fromObject(child);
case "text":
return Text.fromObject(child);
}
}
return child;
}
}
static IsVisible(element, visibleOnly) {
if (!visibleOnly) {
return true;
}
let current = element;
while (current) {
if (typeof current.visible === "boolean" && !current.visible) {
return false;
}
if (typeof current.opacity === "number" && current.opacity <= 0) {
return false;
}
current = current.parent;
}
return true;
}
static VisitForHitTest(group, context, includeGroups, filter, hitOptions, tolerance, stopOnFirst) {
const children = group && group.children;
if (!children) {
return false;
}
const results = context.results;
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (!child) {
continue;
}
if (!_Group.IsVisible(child, context.visibleOnly)) {
continue;
}
const rect = typeof child.getBoundingClientRect === "function" ? child.getBoundingClientRect() : null;
if (rect && !boundsContains(rect, context.x, context.y, tolerance)) {
continue;
}
if (child instanceof _Group) {
if (includeGroups && (!filter || filter(child)) && typeof child.contains === "function" && child.contains(context.x, context.y, hitOptions)) {
results.push(child);
if (stopOnFirst) {
return true;
}
}
if (_Group.VisitForHitTest(
child,
context,
includeGroups,
filter,
hitOptions,
tolerance,
stopOnFirst
)) {
return true;
}
continue;
}
if (!(child instanceof Shape)) {
continue;
}
if (filter && !filter(child)) {
continue;
}
if (typeof child.contains !== "function") {
continue;
}
if (child.contains(context.x, context.y, hitOptions)) {
results.push(child);
if (stopOnFirst) {
return true;
}
}
}
return false;
}
/**
* @name Two.Group#copy
* @function
* @param {Two.Group} [group] - The reference {@link Two.Group}
* @returns {Two.Group}
* @description Copy the properties of one {@link Two.Group} onto another.
*/
copy(group) {
super.copy.call(this, group);
console.warn(
"Two.js: attempting to copy group. Two.Group.children copying not supported."
);
for (let i = 0; i < _Group.Properties.length; i++) {
const k = _Group.Properties[i];
if (k in group) {
this[k] = group[k];
}
}
return this;
}
/**
* @name Two.Group#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Group}
* @description Create a new instance of {@link Two.Group} with the same properties of the current group.
*/
clone(parent) {
const clone = new _Group();
const children = this.children.map(function(child) {
return child.clone();
});
clone.add(children);
clone.opacity = this.opacity;
if (this.mask) {
clone.mask = this.mask;
}
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.className = this.className;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
if (parent) {
parent.add(clone);
}
return clone._update();
}
/**
* @name Two.Group#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the group.
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = "group";
result.children = [];
result.opacity = this.opacity;
result.className = this.className;
result.mask = this.mask ? this.mask.toObject() : null;
_.each(
this.children,
(child, i) => {
result.children[i] = child.toObject();
},
this
);
return result;
}
/**
* @name Two.Group#dispose
* @function
* @returns {Two.Group}
* @description Release the group's renderer resources and detach all events.
* This method recursively disposes all child objects, unbinds the children
* collection events, and preserves the renderer type for potential re-attachment
* to a new renderer.
*/
dispose() {
super.dispose();
if (this.children) {
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (typeof child.dispose === "function") {
child.dispose();
}
}
}
if (this.children && typeof this.children.unbind === "function") {
try {
this.children.unbind();
} catch (e) {
}
}
return this;
}
/**
* @name Two.Group#getShapesAtPoint
* @function
* @param {Number} x - X coordinate in world space.
* @param {Number} y - Y coordinate in world space.
* @param {Object} [options] - Hit test configuration.
* @param {Boolean} [options.visibleOnly=true] - Limit results to visible shapes.
* @param {Boolean} [options.includeGroups=false] - Include groups in the hit results.
* @param {('all'|'deepest')} [options.mode='all'] - Whether to return all intersecting shapes or only the top-most.
* @param {Boolean} [options.deepest] - Alias for `mode: 'deepest'`.
* @param {Number} [options.precision] - Segmentation precision for curved geometry.
* @param {Number} [options.tolerance=0] - Pixel tolerance applied to hit testing.
* @param {Boolean} [options.fill] - Override fill testing behaviour.
* @param {Boolean} [options.stroke] - Override stroke testing behaviour.
* @param {Function} [options.filter] - Predicate to filter shapes from the result set.
* @returns {Shape[]} Ordered list of intersecting shapes, front to back.
* @description Traverse the group hierarchy and return shapes that contain the specified point.
* @nota-bene Expects *world-space coordinates* – the same pixel-space you get from the renderer (e.g., mouse `clientX`/`clientY` adjusted for the canvas’s offset and pixel ratio).
*/
getShapesAtPoint(x, y, options) {
const opts = options || {};
const { results, hitOptions, context, single, empty } = cache.getShapesAtPoint;
results.length = 0;
const mode = opts.mode === "deepest" || opts.deepest ? "deepest" : "all";
const visibleOnly = opts.visibleOnly !== false;
const includeGroups = !!opts.includeGroups;
const filter = typeof opts.filter === "function" ? opts.filter : null;
const tolerance = typeof opts.tolerance === "number" ? opts.tolerance : 0;
if (typeof opts.precision === "number") {
hitOptions.precision = opts.precision;
} else {
delete hitOptions.precision;
}
if (typeof opts.fill !== "undefined") {
hitOptions.fill = opts.fill;
} else {
delete hitOptions.fill;
}
if (typeof opts.stroke !== "undefined") {
hitOptions.stroke = opts.stroke;
} else {
delete hitOptions.stroke;
}
hitOptions.tolerance = tolerance;
hitOptions.ignoreVisibility = !visibleOnly;
const stopOnFirst = mode === "deepest";
context.x = x;
context.y = y;
context.visibleOnly = visibleOnly;
context.results = results;
_Group.VisitForHitTest(
this,
context,
includeGroups,
filter,
hitOptions,
tolerance,
stopOnFirst
);
if (stopOnFirst) {
if (results.length > 0) {
const first = results[0];
results.length = 0;
single[0] = first;
single.length = 1;
return single;
}
empty.length = 0;
return empty;
}
const hits = results.slice();
results.length = 0;
return hits;
}
/**
* @name Two.Group#corner
* @function
* @description Orient the children of the group to the upper left-hand corner of that group.
*/
corner() {
const rect = this.getBoundingClientRect(true);
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.translation.x -= rect.left;
child.translation.y -= rect.top;
}
if (this.mask) {
this.mask.translation.x -= rect.left;
this.mask.translation.y -= rect.top;
}
return this;
}
/**
* @name Two.Group#center
* @function
* @description Orient the children of the group to the center of that group.
*/
center() {
const rect = this.getBoundingClientRect(true);
const cx = rect.left + rect.width / 2 - this.translation.x;
const cy = rect.top + rect.height / 2 - this.translation.y;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (child.isShape) {
child.translation.x -= cx;
child.translation.y -= cy;
}
}
if (this.mask) {
this.mask.translation.x -= cx;
this.mask.translation.y -= cy;
}
return this;
}
/**
* @name Two.Group#getById
* @function
* @description Recursively search for id. Returns the first element found.
* @returns {Two.Shape} - Or `null` if nothing is found.
*/
getById(id) {
let found = null;
function search(node) {
if (node.id === id) {
return node;
} else if (node.children) {
if (node.children.ids[id]) {
return node.children.ids[id];
}
for (let i = 0; i < node.children.length; i++) {
found = search(node.children[i]);
if (found) {
return found;
}
}
}
return null;
}
return search(this);
}
/**
* @name Two.Group#getByClassName
* @function
* @description Recursively search for classes. Returns an array of matching elements.
* @returns {Two.Shape[]} - Or empty array if nothing is found.
*/
getByClassName(className) {
const found = [];
function search(node) {
if (Array.prototype.indexOf.call(node.classList, className) >= 0) {
found.push(node);
}
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
search(child);
}
}
return found;
}
return search(this);
}
/**
* @name Two.Group#getByType
* @function
* @description Recursively search for children of a specific type, e.g. {@link Two.Path}. Pass a reference to this type as the param. Returns an array of matching elements.
* @returns {Two.Shape[]} - Empty array if nothing is found.
*/
getByType(type) {
const found = [];
function search(node) {
if (node instanceof type) {
found.push(node);
}
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
search(child);
}
}
return found;
}
return search(this);
}
/**
* @name Two.Group#add
* @function
* @param {Two.Shape[]|...Two.Shape} objects - An array of objects to be added. Can also be supplied as individual arguments.
* @description Add objects to the group.
*/
add(objects) {
if (!(objects instanceof Array)) {
objects = Array.prototype.slice.call(arguments);
} else {
objects = objects.slice();
}
for (let i = 0; i < objects.length; i++) {
const child = objects[i];
if (!(child && child.id)) {
continue;
}
const index = Array.prototype.indexOf.call(this.children, child);
if (index >= 0) {
this.children.splice(index, 1);
}
this.children.push(child);
}
return this;
}
/**
* @name Two.Group#remove
* @function
* @param {Two.Shape[]|...Two.Shape} [objects=self] - An array of objects to be removed. Can be also removed as individual arguments. If no arguments are passed, then it removes itself from its parent.
* @description Remove objects from the group.
*/
remove(objects) {
const l = arguments.length, grandparent = this.parent;
if (l <= 0 && grandparent) {
grandparent.remove(this);
return this;
}
if (!(objects instanceof Array)) {
objects = Array.prototype.slice.call(arguments);
} else {
objects = objects.slice();
}
for (let i = 0; i < objects.length; i++) {
const object = objects[i];
if (!object || !this.children.ids[object.id]) {
continue;
}
const index = this.children.indexOf(object);
if (index >= 0) {
this.children.splice(index, 1);
}
}
return this;
}
/**
* @name Two.Group#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the group.
*/
getBoundingClientRect(shallow) {
let rect, matrix, tc, lc, rc, bc;
this._update(true);
let left = Infinity, right = -Infinity, top = Infinity, bottom = -Infinity;
const regex3 = /texture|gradient/i;
matrix = shallow ? this.matrix : this.worldMatrix;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (!child.visible || regex3.test(child._renderer.type)) {
continue;
}
rect = child.getBoundingClientRect(shallow);
tc = typeof rect.top !== "number" || _.isNaN(rect.top) || !isFinite(rect.top);
lc = typeof rect.left !== "number" || _.isNaN(rect.left) || !isFinite(rect.left);
rc = typeof rect.right !== "number" || _.isNaN(rect.right) || !isFinite(rect.right);
bc = typeof rect.bottom !== "number" || _.isNaN(rect.bottom) || !isFinite(rect.bottom);
if (tc || lc || rc || bc) {
continue;
}
if (shallow) {
const [ax, ay] = matrix.multiply(rect.left, rect.top);
const [bx, by] = matrix.multiply(rect.right, rect.top);
const [cx, cy] = matrix.multiply(rect.left, rect.bottom);
const [dx, dy] = matrix.multiply(rect.right, rect.bottom);
top = min3(ay, by, cy, dy, top);
left = min3(ax, bx, cx, dx, left);
right = max3(ax, bx, cx, dx, right);
bottom = max3(ay, by, cy, dy, bottom);
} else {
top = min3(rect.top, top);
left = min3(rect.left, left);
right = max3(rect.right, right);
bottom = max3(rect.bottom, bottom);
}
}
return {
top,
left,
right,
bottom,
width: right - left,
height: bottom - top
};
}
/**
* @name Two.Group#noFill
* @function
* @description Apply `noFill` method to all child shapes.
*/
noFill() {
this.children.forEach(function(child) {
child.noFill();
});
return this;
}
/**
* @name Two.Group#noStroke
* @function
* @description Apply `noStroke` method to all child shapes.
*/
noStroke() {
this.children.forEach(function(child) {
child.noStroke();
});
return this;
}
/**
* @name Two.Group#subdivide
* @function
* @description Apply `subdivide` method to all child shapes.
*/
subdivide() {
const args = arguments;
this.children.forEach(function(child) {
child.subdivide.apply(child, args);
});
return this;
}
/**
* @name Two.Group#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
let i, l, child;
if (this._flagBeginning || this._flagEnding) {
const beginning = Math.min(this._beginning, this._ending);
const ending = Math.max(this._beginning, this._ending);
const length = this.length;
let sum = 0;
const bd = beginning * length;
const ed = ending * length;
for (i = 0; i < this.children.length; i++) {
child = this.children[i];
l = child.length;
if (bd > sum + l) {
child.beginning = 1;
child.ending = 1;
} else if (ed < sum) {
child.beginning = 0;
child.ending = 0;
} else if (bd > sum && bd < sum + l) {
child.beginning = (bd - sum) / l;
child.ending = 1;
} else if (ed > sum && ed < sum + l) {
child.beginning = 0;
child.ending = (ed - sum) / l;
} else {
child.beginning = 0;
child.ending = 1;
}
sum += l;
}
}
return super._update.apply(this, arguments);
}
/**
* @name Two.Group#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
if (this._flagAdditions) {
this.additions.length = 0;
this._flagAdditions = false;
}
if (this._flagSubtractions) {
this.subtractions.length = 0;
this._flagSubtractions = false;
}
this._flagOrder = this._flagMask = this._flagOpacity = this._flagBeginning = this._flagEnding = false;
super.flagReset.call(this);
return this;
}
};
var proto22 = {
visible: {
enumerable: true,
get: function() {
return this._visible;
},
set: function(v) {
this._flagVisible = this._visible !== v || this._flagVisible;
this._visible = v;
}
},
opacity: {
enumerable: true,
get: function() {
return this._opacity;
},
set: function(v) {
this._flagOpacity = this._opacity !== v || this._flagOpacity;
this._opacity = v;
}
},
beginning: {
enumerable: true,
get: function() {
return this._beginning;
},
set: function(v) {
this._flagBeginning = this._beginning !== v || this._flagBeginning;
this._beginning = v;
}
},
ending: {
enumerable: true,
get: function() {
return this._ending;
},
set: function(v) {
this._flagEnding = this._ending !== v || this._flagEnding;
this._ending = v;
}
},
length: {
enumerable: true,
get: function() {
if (this._flagLength || this._length <= 0) {
this._length = 0;
if (!this.children) {
return this._length;
}
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
this._length += child.length;
}
}
return this._length;
}
},
fill: {
enumerable: true,
get: function() {
return this._fill;
},
set: function(v) {
this._fill = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.fill = v;
}
}
},
stroke: {
enumerable: true,
get: function() {
return this._stroke;
},
set: function(v) {
this._stroke = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.stroke = v;
}
}
},
linewidth: {
enumerable: true,
get: function() {
return this._linewidth;
},
set: function(v) {
this._linewidth = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.linewidth = v;
}
}
},
join: {
enumerable: true,
get: function() {
return this._join;
},
set: function(v) {
this._join = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.join = v;
}
}
},
miter: {
enumerable: true,
get: function() {
return this._miter;
},
set: function(v) {
this._miter = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.miter = v;
}
}
},
cap: {
enumerable: true,
get: function() {
return this._cap;
},
set: function(v) {
this._cap = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.cap = v;
}
}
},
closed: {
enumerable: true,
get: function() {
return this._closed;
},
set: function(v) {
this._closed = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.closed = v;
}
}
},
curved: {
enumerable: true,
get: function() {
return this._curved;
},
set: function(v) {
this._curved = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.curved = v;
}
}
},
automatic: {
enumerable: true,
get: function() {
return this._automatic;
},
set: function(v) {
this._automatic = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.automatic = v;
}
}
},
children: {
enumerable: true,
get: function() {
return this._children;
},
set: function(children) {
const insertChildren = Group.InsertChildren.bind(this);
const removeChildren = Group.RemoveChildren.bind(this);
const orderChildren = Group.OrderChildren.bind(this);
if (this._children) {
this._children.unbind();
if (this._children.length > 0) {
removeChildren(this._children);
}
}
this._children = new Children(children);
this._children.bind(Events.Types.insert, insertChildren);
this._children.bind(Events.Types.remove, removeChildren);
this._children.bind(Events.Types.order, orderChildren);
if (children.length > 0) {
insertChildren(children);
}
}
},
mask: {
enumerable: true,
get: function() {
return this._mask;
},
set: function(v) {
this._mask = v;
this._flagMask = true;
if (_.isObject(v) && !v.clip) {
v.clip = true;
}
}
},
/**
* @name Two.Group#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space for all child shapes.
* @description When `strokeAttenuation` is `false`, this property is applied to all child shapes, making their stroke widths automatically adjust to compensate for the group's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke widths scale normally with transformations.
*/
strokeAttenuation: {
enumerable: true,
get: function() {
return this._strokeAttenuation;
},
set: function(v) {
this._strokeAttenuation = !!v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (child.strokeAttenuation !== void 0) {
child.strokeAttenuation = v;
}
}
}
}
};
function replaceParent(child, newParent) {
const parent = child.parent;
let index;
if (parent === newParent) {
add();
return;
}
if (parent && parent.children.ids[child.id]) {
index = Array.prototype.indexOf.call(parent.children, child);
parent.children.splice(index, 1);
splice();
}
if (newParent) {
add();
return;
}
splice();
if (parent._flagAdditions && parent.additions.length === 0) {
parent._flagAdditions = false;
}
if (parent._flagSubtractions && parent.subtractions.length === 0) {
parent._flagSubtractions = false;
}
delete child.parent;
function add() {
if (newParent.subtractions.length > 0) {
index = Array.prototype.indexOf.call(newParent.subtractions, child);
if (index >= 0) {
newParent.subtractions.splice(index, 1);
}
}
if (newParent.additions.length > 0) {
index = Array.prototype.indexOf.call(newParent.additions, child);
if (index >= 0) {
newParent.additions.splice(index, 1);
}
}
child.parent = newParent;
newParent.additions.push(child);
newParent._flagAdditions = true;
}
function splice() {
index = Array.prototype.indexOf.call(parent.additions, child);
if (index >= 0) {
parent.additions.splice(index, 1);
}
index = Array.prototype.indexOf.call(parent.subtractions, child);
if (index < 0) {
parent.subtractions.push(child);
parent._flagSubtractions = true;
}
}
}
// src/shapes/line.js
var Line = class extends Path {
constructor(x1, y1, x2, y2) {
const points = [new Anchor(x1, y1), new Anchor(x2, y2)];
super(points);
for (let prop in proto23) {
Object.defineProperty(this, prop, proto23[prop]);
}
this.vertices[0].command = Commands.move;
this.vertices[1].command = Commands.line;
this.automatic = false;
}
static Properties = ["left", "right"];
};
var proto23 = {
left: {
enumerable: true,
get: function() {
return this.vertices[0];
},
set: function(v) {
if (_.isObject(v)) {
this.vertices.splice(0, 1, v);
this.vertices[0].command = Commands.move;
} else {
const error = new TwoError("Two.Line.left argument is not an object.");
console.warn(error.name, error.message);
}
}
},
right: {
enumerable: true,
get: function() {
return this.vertices[1];
},
set: function(v) {
if (_.isObject(v)) {
this.vertices.splice(1, 1, v);
this.vertices[1].command = Commands.line;
} else {
const error = new TwoError("Two.Line.right argument is not an object.");
console.warn(error.name, error.message);
}
}
}
};
// src/utils/interpret-svg.js
var regex2 = {
path: /[+-]?(?:\d*\.\d+|\d+)(?:[eE][+-]\d+)?/g,
cssBackgroundImage: /url\(['"]?#([\w\d-_]*)['"]?\)/i,
unitSuffix: /[a-zA-Z%]*/i
};
var alignments = {
start: "left",
middle: "center",
end: "right"
};
var reservedAttributesToRemove = [
"id",
"class",
"transform",
"xmlns",
"viewBox"
];
var overwriteAttrs = ["x", "y", "width", "height", "href", "xlink:href"];
function getAlignment(anchor2) {
return alignments[anchor2];
}
function getBaseline(node) {
const a = node.getAttribute("dominant-baseline");
const b = node.getAttribute("alignment-baseline");
return a || b;
}
function getTagName(tag) {
return tag.replace(/svg:/gi, "").toLowerCase();
}
function applyTransformsToVector(transforms, vector3) {
vector3.x += transforms.translateX;
vector3.y += transforms.translateY;
vector3.x *= transforms.scaleX;
vector3.y *= transforms.scaleY;
if (transforms.rotation !== 0) {
const l = vector3.length();
vector3.x = l * Math.cos(transforms.rotation);
vector3.y = l * Math.sin(transforms.rotation);
}
}
function extractCSSText(text, styles) {
if (!styles) {
styles = {};
}
const commands = text.split(";");
for (let i = 0; i < commands.length; i++) {
const command = commands[i].split(":");
const name = command[0];
const value = command[1];
if (typeof name === "undefined" || typeof value === "undefined") {
continue;
}
const trimmedName = name.replace(/\s/g, "");
const trimmedValue = value.replace(/\s/g, "");
styles[trimmedName] = trimmedValue;
}
return styles;
}
function getSvgStyles(node) {
const styles = {};
const attributes = getSvgAttributes(node);
const length = Math.max(attributes.length, node.style.length);
for (let i = 0; i < length; i++) {
const command = node.style[i];
const attribute = attributes[i];
if (command) {
styles[command] = node.style[command];
}
if (attribute) {
styles[attribute] = node.getAttribute(attribute);
}
}
return styles;
}
function getSvgAttributes(node) {
const attributes = node.getAttributeNames();
for (let i = 0; i < reservedAttributesToRemove.length; i++) {
const keyword = reservedAttributesToRemove[i];
const index = Array.prototype.indexOf.call(attributes, keyword);
if (index >= 0) {
attributes.splice(index, 1);
}
}
return attributes;
}
function applySvgViewBox(node, value) {
const elements = value.split(/[\s,]/);
const x = -parseFloat(elements[0]);
const y = -parseFloat(elements[1]);
const width = parseFloat(elements[2]);
const height = parseFloat(elements[3]);
if (x && y) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if ("translation" in child) {
child.translation.add(x, y);
} else if ("x" in child) {
child.x = x;
} else if ("y" in child) {
child.y = y;
}
}
}
const xExists = typeof node.x === "number";
const yExists = typeof node.y === "number";
const widthExists = typeof node.width === "number";
const heightExists = typeof node.height === "number";
if (xExists) {
node.translation.x += node.x;
}
if (yExists) {
node.translation.y += node.y;
}
if (widthExists || heightExists) {
node.scale = new Vector(1, 1);
}
if (widthExists) {
node.scale.x = node.width / width;
}
if (heightExists) {
node.scale.y = node.height / height;
}
node.mask = new Rectangle(0, 0, width, height);
node.mask.origin.set(-width / 2, -height / 2);
return node;
}
function applySvgAttributes(node, elem, parentStyles) {
const styles = {}, attributes = {}, extracted = {};
let i, m, key, value, prop, attr;
let transforms, x, y;
let id, scene, ref, tagName;
let ca, cb, cc, error;
if (node === null) {
return styles;
}
if (root.getComputedStyle) {
const computedStyles = root.getComputedStyle(node);
i = computedStyles.length;
while (i--) {
key = computedStyles[i];
value = computedStyles[key];
if (typeof value !== "undefined") {
styles[key] = value;
}
}
}
for (i = 0; i < node.attributes.length; i++) {
attr = node.attributes[i];
if (/style/i.test(attr.nodeName)) {
extractCSSText(attr.value, extracted);
} else {
attributes[attr.nodeName] = attr.value;
}
}
if (typeof styles.opacity !== "undefined") {
styles["stroke-opacity"] = styles.opacity;
styles["fill-opacity"] = styles.opacity;
delete styles.opacity;
}
if (parentStyles) {
_.defaults(styles, parentStyles);
}
_.extend(styles, extracted, attributes);
styles.visible = !(typeof styles.display === "undefined" && /none/i.test(styles.display)) || typeof styles.visibility === "undefined" && /hidden/i.test(styles.visibility);
for (key in styles) {
value = styles[key];
switch (key) {
case "gradientTransform":
if (/none/i.test(value)) break;
m = node.gradientTransform && node.gradientTransform.baseVal && node.gradientTransform.baseVal.length > 0 ? node.gradientTransform.baseVal[0].matrix : node.getCTM ? node.getCTM() : null;
if (m === null) break;
transforms = decomposeMatrix(m);
switch (elem._renderer.type) {
case "linear-gradient":
applyTransformsToVector(transforms, elem.left);
applyTransformsToVector(transforms, elem.right);
break;
case "radial-gradient":
elem.center.x += transforms.translateX;
elem.center.y += transforms.translateY;
elem.focal.x += transforms.translateX;
elem.focal.y += transforms.translateY;
elem.radius *= Math.max(transforms.scaleX, transforms.scaleY);
break;
}
break;
case "transform":
if (/none/i.test(value)) break;
m = node.transform && node.transform.baseVal && node.transform.baseVal.length > 0 ? node.transform.baseVal[0].matrix : node.getCTM ? node.getCTM() : null;
if (m === null) break;
if (Constants.AutoCalculateImportedMatrices) {
transforms = decomposeMatrix(m);
elem.translation.set(transforms.translateX, transforms.translateY);
elem.rotation = Math.PI * (transforms.rotation / 180);
elem.scale = new Vector(transforms.scaleX, transforms.scaleY);
x = parseFloat((styles.x + "").replace("px"));
y = parseFloat((styles.y + "").replace("px"));
if (x) {
elem.translation.x = x;
}
if (y) {
elem.translation.y = y;
}
} else {
m = node.getCTM();
elem._matrix.manual = true;
elem._matrix.set(m.a, m.b, m.c, m.d, m.e, m.f);
}
break;
case "visible":
if (elem instanceof Group) {
elem._visible = value;
break;
}
elem.visible = value;
break;
case "stroke-linecap":
if (elem instanceof Group) {
elem._cap = value;
break;
}
elem.cap = value;
break;
case "stroke-linejoin":
if (elem instanceof Group) {
elem._join = value;
break;
}
elem.join = value;
break;
case "stroke-miterlimit":
if (elem instanceof Group) {
elem._miter = value;
break;
}
elem.miter = value;
break;
case "stroke-width":
if (elem instanceof Group) {
elem._linewidth = parseFloat(value);
break;
}
elem.linewidth = parseFloat(value);
break;
case "opacity":
case "stroke-opacity":
case "fill-opacity":
if (elem instanceof Group) {
elem._opacity = parseFloat(value);
break;
}
elem.opacity = parseFloat(value);
break;
case "clip-path":
if (regex2.cssBackgroundImage.test(value)) {
id = value.replace(regex2.cssBackgroundImage, "$1");
if (read.defs.current && read.defs.current.contains(id)) {
ref = read.defs.current.get(id);
if (ref && ref.childNodes.length > 0) {
ref = ref.childNodes[0];
tagName = getTagName(ref.nodeName);
elem.mask = read[tagName].call(this, ref, {});
switch (elem._renderer.type) {
case "text":
case "path":
elem.position.add(elem.mask.position);
elem.mask.position.clear();
break;
}
}
}
}
break;
case "fill":
case "stroke":
prop = (elem instanceof Group ? "_" : "") + key;
if (regex2.cssBackgroundImage.test(value)) {
id = value.replace(regex2.cssBackgroundImage, "$1");
if (read.defs.current && read.defs.current.contains(id)) {
ref = read.defs.current.get(id);
if (!ref.object) {
tagName = getTagName(ref.nodeName);
ref.object = read[tagName].call(this, ref, {});
}
ref = ref.object;
} else {
scene = getScene(this);
ref = scene.getById(id);
}
elem[prop] = ref;
} else {
elem[prop] = value;
}
break;
case "id":
elem.id = value;
break;
case "class":
case "className":
elem.classList = value.split(" ");
elem._flagClassName = true;
break;
case "x":
case "y":
ca = elem instanceof Gradient;
cb = elem instanceof LinearGradient;
cc = elem instanceof RadialGradient;
if (ca || cb || cc) {
break;
}
if (value.match("[a-z%]$") && !value.endsWith("px")) {
error = new TwoError(
"only pixel values are supported with the " + key + " attribute."
);
console.warn(error.name, error.message);
}
elem.translation[key] = parseFloat(value);
break;
case "font-family":
if (elem instanceof Text) {
elem.family = value;
}
break;
case "font-size":
if (elem instanceof Text) {
if (value.match("[a-z%]$") && !value.endsWith("px")) {
error = new TwoError(
"only pixel values are supported with the " + key + " attribute."
);
console.warn(error.name, error.message);
}
elem.size = parseFloat(value);
}
break;
case "font-weight":
if (elem instanceof Text) {
elem.weight = value;
}
break;
case "font-style":
if (elem instanceof Text) {
elem.style = value;
}
break;
case "text-decoration":
if (elem instanceof Text) {
elem.decoration = value;
}
break;
case "line-height":
if (elem instanceof Text) {
elem.leading = value;
}
break;
}
}
if (Object.keys(node.dataset).length) elem.dataset = node.dataset;
return styles;
}
function updateDefsCache(node, defsCache) {
for (let i = 0, l = node.childNodes.length; i < l; i++) {
const n = node.childNodes[i];
if (!n.id) continue;
const tagName = getTagName(node.nodeName);
if (tagName === "#text") continue;
defsCache.add(n.id, n);
}
}
function getScene(node) {
while (node.parent) {
node = node.parent;
}
return node.scene;
}
var read = {
svg: function(node) {
const defs = read.defs.current = new Registry();
const elements = node.getElementsByTagName("defs");
for (let i = 0; i < elements.length; i++) {
updateDefsCache(elements[i], defs);
}
const svg2 = read.g.call(this, node);
const viewBox = node.getAttribute("viewBox");
const x = node.getAttribute("x");
const y = node.getAttribute("y");
const width = node.getAttribute("width");
const height = node.getAttribute("height");
svg2.defs = defs;
const viewBoxExists = viewBox !== null;
const xExists = x !== null;
const yExists = y !== null;
const widthExists = width !== null;
const heightExists = height !== null;
if (xExists) {
svg2.x = parseFloat(x.replace(regex2.unitSuffix, ""));
}
if (yExists) {
svg2.y = parseFloat(y.replace(regex2.unitSuffix, ""));
}
if (widthExists) {
svg2.width = parseFloat(width.replace(regex2.unitSuffix, ""));
}
if (heightExists) {
svg2.height = parseFloat(height.replace(regex2.unitSuffix, ""));
}
if (viewBoxExists) {
applySvgViewBox(svg2, viewBox);
}
delete read.defs.current;
return svg2;
},
defs: function(node) {
return null;
},
use: function(node, styles) {
let error;
const href = node.getAttribute("href") || node.getAttribute("xlink:href");
if (!href) {
error = new TwoError("encountered with no href.");
console.warn(error.name, error.message);
return null;
}
const id = href.slice(1);
if (!read.defs.current.contains(id)) {
error = new TwoError(
"unable to find element for reference " + href + "."
);
console.warn(error.name, error.message);
return null;
}
const template = read.defs.current.get(id);
const fullNode = template.cloneNode(true);
for (let i = 0; i < node.attributes.length; i++) {
const attr = node.attributes[i];
const ca = overwriteAttrs.includes(attr.nodeName);
const cb = !fullNode.hasAttribute(attr.nodeName);
if (ca || cb) {
fullNode.setAttribute(attr.nodeName, attr.value);
}
}
const tagName = getTagName(fullNode.nodeName);
return read[tagName].call(this, fullNode, styles);
},
g: function(node, parentStyles) {
const group = new Group();
applySvgAttributes.call(this, node, group, parentStyles);
this.add(group);
const styles = getSvgStyles.call(this, node);
for (let i = 0, l = node.childNodes.length; i < l; i++) {
const n = node.childNodes[i];
const tag = n.nodeName;
if (!tag) return;
const tagName = getTagName(tag);
if (tagName in read) {
const o = read[tagName].call(group, n, styles);
if (!!o && !o.parent) {
group.add(o);
}
}
}
return group;
},
polygon: function(node, parentStyles) {
let points;
if (typeof node === "string") {
points = node;
} else {
points = node.getAttribute("points");
}
const verts = [];
points.replace(
/(-?[\d.eE-]+)[,|\s](-?[\d.eE-]+)/g,
function(match, p1, p2) {
verts.push(new Anchor(parseFloat(p1), parseFloat(p2)));
}
);
const poly = new Path(verts, true);
poly.stroke = "none";
poly.fill = "black";
applySvgAttributes.call(this, node, poly, parentStyles);
return poly;
},
polyline: function(node, parentStyles) {
const poly = read.polygon.call(this, node, parentStyles);
poly.closed = false;
return poly;
},
path: function(node, parentStyles) {
let path;
if (typeof node === "string") {
path = node;
node = null;
} else {
path = node.getAttribute("d");
}
let points = [];
let closed2 = false, relative = false;
if (path) {
let coord = new Anchor();
let control, coords;
let commands = path.match(/[a-df-z][^a-df-z]*/gi);
const last = commands.length - 1;
_.each(commands.slice(0), function(command, i) {
const items = command.slice(1).trim().match(regex2.path);
const type = command[0];
const lower = type.toLowerCase();
let bin, j, l, ct, times;
const result = [];
if (i === 0) {
commands = [];
}
switch (lower) {
case "h":
case "v":
if (items.length > 1) {
bin = 1;
}
break;
case "m":
case "l":
case "t":
if (items.length > 2) {
bin = 2;
}
break;
case "s":
case "q":
if (items.length > 4) {
bin = 4;
}
break;
case "c":
if (items.length > 6) {
bin = 6;
}
break;
case "a":
if (items.length > 7) {
bin = 7;
}
break;
}
if (bin) {
for (j = 0, l = items.length, times = 0; j < l; j += bin) {
ct = type;
if (times > 0) {
switch (type) {
case "m":
ct = "l";
break;
case "M":
ct = "L";
break;
}
}
result.push(ct + items.slice(j, j + bin).join(" "));
times++;
}
commands = Array.prototype.concat.apply(commands, result);
} else {
commands.push(command);
}
});
_.each(commands, function(command, i) {
let result, x, y;
const type = command[0];
const lower = type.toLowerCase();
coords = command.slice(1).trim().match(regex2.path);
relative = type === lower;
let x1, y1, x2, y2, x3, y3, x4, y4, reflection;
let a, b;
let anchor2, rx, ry, xAxisRotation, largeArcFlag, sweepFlag;
switch (lower) {
case "z":
if (i >= last) {
closed2 = true;
} else {
x = coord.x;
y = coord.y;
result = new Anchor(
x,
y,
void 0,
void 0,
void 0,
void 0,
Commands.close
);
for (let j = points.length - 1; j >= 0; j--) {
const point = points[j];
if (/m/i.test(point.command)) {
coord = point;
break;
}
}
}
break;
case "m":
case "l":
control = void 0;
x = parseFloat(coords[0]);
y = parseFloat(coords[1]);
result = new Anchor(
x,
y,
void 0,
void 0,
void 0,
void 0,
/m/i.test(lower) ? Commands.move : Commands.line
);
if (relative) {
result.addSelf(coord);
}
coord = result;
break;
case "h":
case "v":
a = /h/i.test(lower) ? "x" : "y";
b = /x/i.test(a) ? "y" : "x";
result = new Anchor(
void 0,
void 0,
void 0,
void 0,
void 0,
void 0,
Commands.line
);
result[a] = parseFloat(coords[0]);
result[b] = coord[b];
if (relative) {
result[a] += coord[a];
}
coord = result;
break;
case "c":
case "s":
x1 = coord.x;
y1 = coord.y;
if (!control) {
control = new Vector();
}
if (/c/i.test(lower)) {
x2 = parseFloat(coords[0]);
y2 = parseFloat(coords[1]);
x3 = parseFloat(coords[2]);
y3 = parseFloat(coords[3]);
x4 = parseFloat(coords[4]);
y4 = parseFloat(coords[5]);
} else {
reflection = getReflection(coord, control, relative);
x2 = reflection.x;
y2 = reflection.y;
x3 = parseFloat(coords[0]);
y3 = parseFloat(coords[1]);
x4 = parseFloat(coords[2]);
y4 = parseFloat(coords[3]);
}
if (relative) {
x2 += x1;
y2 += y1;
x3 += x1;
y3 += y1;
x4 += x1;
y4 += y1;
}
coord.controls.right.set(x2 - coord.x, y2 - coord.y);
result = new Anchor(
x4,
y4,
x3 - x4,
y3 - y4,
void 0,
void 0,
Commands.curve
);
coord = result;
control = result.controls.left;
break;
case "t":
case "q":
x1 = coord.x;
y1 = coord.y;
if (!control) {
control = new Vector();
}
if (/q/i.test(lower)) {
x2 = parseFloat(coords[0]);
y2 = parseFloat(coords[1]);
x3 = parseFloat(coords[0]);
y3 = parseFloat(coords[1]);
x4 = parseFloat(coords[2]);
y4 = parseFloat(coords[3]);
} else {
reflection = getReflection(coord, control, relative);
x2 = reflection.x;
y2 = reflection.y;
x3 = reflection.x;
y3 = reflection.y;
x4 = parseFloat(coords[0]);
y4 = parseFloat(coords[1]);
}
if (relative) {
x2 += x1;
y2 += y1;
x3 += x1;
y3 += y1;
x4 += x1;
y4 += y1;
}
coord.controls.right.set(
(x2 - coord.x) * 0.33,
(y2 - coord.y) * 0.33
);
result = new Anchor(
x4,
y4,
x3 - x4,
y3 - y4,
void 0,
void 0,
Commands.curve
);
coord = result;
control = result.controls.left;
break;
case "a":
x1 = coord.x;
y1 = coord.y;
rx = parseFloat(coords[0]);
ry = parseFloat(coords[1]);
xAxisRotation = parseFloat(coords[2]);
largeArcFlag = parseFloat(coords[3]);
sweepFlag = parseFloat(coords[4]);
x4 = parseFloat(coords[5]);
y4 = parseFloat(coords[6]);
if (relative) {
x4 += x1;
y4 += y1;
}
anchor2 = new Anchor(x4, y4);
anchor2.command = Commands.arc;
anchor2.rx = rx;
anchor2.ry = ry;
anchor2.xAxisRotation = xAxisRotation;
anchor2.largeArcFlag = largeArcFlag;
anchor2.sweepFlag = sweepFlag;
result = anchor2;
coord = anchor2;
control = void 0;
break;
}
if (result) {
if (Array.isArray(result)) {
points = points.concat(result);
} else {
points.push(result);
}
}
});
}
path = new Path(points, closed2, void 0, true);
path.stroke = "none";
path.fill = "black";
const rect = path.getBoundingClientRect(true);
rect.centroid = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
_.each(path.vertices, function(v) {
v.subSelf(rect.centroid);
});
applySvgAttributes.call(this, node, path, parentStyles);
path.translation.addSelf(rect.centroid);
return path;
},
circle: function(node, parentStyles) {
const x = parseFloat(node.getAttribute("cx"));
const y = parseFloat(node.getAttribute("cy"));
const r = parseFloat(node.getAttribute("r"));
const circle = new Circle(0, 0, r);
circle.stroke = "none";
circle.fill = "black";
applySvgAttributes.call(this, node, circle, parentStyles);
circle.translation.x = x;
circle.translation.y = y;
return circle;
},
ellipse: function(node, parentStyles) {
const x = parseFloat(node.getAttribute("cx"));
const y = parseFloat(node.getAttribute("cy"));
const width = parseFloat(node.getAttribute("rx"));
const height = parseFloat(node.getAttribute("ry"));
const ellipse = new Ellipse(0, 0, width, height);
ellipse.stroke = "none";
ellipse.fill = "black";
applySvgAttributes.call(this, node, ellipse, parentStyles);
ellipse.translation.x = x;
ellipse.translation.y = y;
return ellipse;
},
rect: function(node, parentStyles) {
const rx = parseFloat(node.getAttribute("rx"));
const ry = parseFloat(node.getAttribute("ry"));
if (!_.isNaN(rx) || !_.isNaN(ry)) {
return read["rounded-rect"](node);
}
const width = parseFloat(node.getAttribute("width"));
const height = parseFloat(node.getAttribute("height"));
const w2 = width / 2;
const h2 = height / 2;
const rect = new Rectangle(0, 0, width, height);
rect.stroke = "none";
rect.fill = "black";
applySvgAttributes.call(this, node, rect, parentStyles);
rect.translation.x += w2;
rect.translation.y += h2;
return rect;
},
"rounded-rect": function(node, parentStyles) {
const rx = parseFloat(node.getAttribute("rx")) || 0;
const ry = parseFloat(node.getAttribute("ry")) || 0;
const width = parseFloat(node.getAttribute("width"));
const height = parseFloat(node.getAttribute("height"));
const w2 = width / 2;
const h2 = height / 2;
const radius = new Vector(rx, ry);
const rect = new RoundedRectangle(0, 0, width, height, radius);
rect.stroke = "none";
rect.fill = "black";
applySvgAttributes.call(this, node, rect, parentStyles);
rect.translation.x += w2;
rect.translation.y += h2;
return rect;
},
line: function(node, parentStyles) {
const x1 = parseFloat(node.getAttribute("x1"));
const y1 = parseFloat(node.getAttribute("y1"));
const x2 = parseFloat(node.getAttribute("x2"));
const y2 = parseFloat(node.getAttribute("y2"));
const line = new Line(x1, y1, x2, y2).noFill();
applySvgAttributes.call(this, node, line, parentStyles);
return line;
},
lineargradient: function(node, parentStyles) {
let units = node.getAttribute("gradientUnits");
let spread = node.getAttribute("spreadMethod");
if (!units) {
units = "objectBoundingBox";
}
if (!spread) {
spread = "pad";
}
let x1 = parseFloat(node.getAttribute("x1") || 0);
let y1 = parseFloat(node.getAttribute("y1") || 0);
let x2 = parseFloat(node.getAttribute("x2") || 0);
let y2 = parseFloat(node.getAttribute("y2") || 0);
const ox = (x2 + x1) / 2;
const oy = (y2 + y1) / 2;
if (/userSpaceOnUse/i.test(units)) {
x1 -= ox;
y1 -= oy;
x2 -= ox;
y2 -= oy;
}
const stops = [];
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
let offset = child.getAttribute("offset");
if (/%/gi.test(offset)) {
offset = parseFloat(offset.replace(/%/gi, "")) / 100;
}
offset = parseFloat(offset);
let color = child.getAttribute("stop-color");
let opacity = child.getAttribute("stop-opacity");
let style = child.getAttribute("style");
let matches;
if (color === null) {
matches = style ? style.match(/stop-color:\s?([#a-fA-F0-9]*)/) : false;
color = matches && matches.length > 1 ? matches[1] : void 0;
}
if (opacity === null) {
matches = style ? style.match(/stop-opacity:\s?([0-9.-]*)/) : false;
opacity = matches && matches.length > 1 ? parseFloat(matches[1]) : 1;
} else {
opacity = parseFloat(opacity);
}
stops.push(new Stop(offset, color, opacity));
}
const gradient = new LinearGradient(x1, y1, x2, y2, stops);
gradient.spread = spread;
gradient.units = units;
applySvgAttributes.call(this, node, gradient, parentStyles);
return gradient;
},
radialgradient: function(node, parentStyles) {
let units = node.getAttribute("gradientUnits");
let spread = node.getAttribute("spreadMethod");
if (!units) {
units = "objectBoundingBox";
}
if (!spread) {
spread = "pad";
}
let cx = parseFloat(node.getAttribute("cx")) || 0;
let cy = parseFloat(node.getAttribute("cy")) || 0;
let r = parseFloat(node.getAttribute("r"));
let fx = parseFloat(node.getAttribute("fx"));
let fy = parseFloat(node.getAttribute("fy"));
if (_.isNaN(fx)) {
fx = cx;
}
if (_.isNaN(fy)) {
fy = cy;
}
const ox = Math.abs(cx + fx) / 2;
const oy = Math.abs(cy + fy) / 2;
if (/userSpaceOnUse/i.test(units)) {
cx -= ox;
cy -= oy;
fx -= ox;
fy -= oy;
}
const stops = [];
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
let offset = child.getAttribute("offset");
if (/%/gi.test(offset)) {
offset = parseFloat(offset.replace(/%/gi, "")) / 100;
}
offset = parseFloat(offset);
let color = child.getAttribute("stop-color");
let opacity = child.getAttribute("stop-opacity");
let style = child.getAttribute("style");
let matches;
if (color === null) {
matches = style ? style.match(/stop-color:\s?([#a-fA-F0-9]*)/) : false;
color = matches && matches.length > 1 ? matches[1] : void 0;
}
if (opacity === null) {
matches = style ? style.match(/stop-opacity:\s?([0-9.-]*)/) : false;
opacity = matches && matches.length > 1 ? parseFloat(matches[1]) : 1;
} else {
opacity = parseFloat(opacity);
}
stops.push(new Stop(offset, color, opacity));
}
const gradient = new RadialGradient(cx, cy, r, stops, fx, fy);
gradient.spread = spread;
gradient.units = units;
applySvgAttributes.call(this, node, gradient, parentStyles);
return gradient;
},
text: function(node, parentStyles) {
const alignment = getAlignment(node.getAttribute("text-anchor")) || "left";
const baseline = getBaseline(node) || "baseline";
let message = "";
if (node.childNodes.length > 0 && node.childNodes[0].tagName === "TSPAN") {
message = node.childNodes[0].textContent;
} else {
message = node.textContent;
}
const text = new Text(message);
applySvgAttributes.call(this, node, text, parentStyles);
text.alignment = alignment;
text.baseline = baseline;
return text;
},
clippath: function(node, parentStyles) {
if (read.defs.current && !read.defs.current.contains(node.id)) {
read.defs.current.add(node.id, node);
}
return null;
},
image: function(node, parentStyles) {
let error;
const href = node.getAttribute("href") || node.getAttribute("xlink:href");
if (!href) {
error = new TwoError("encountered with no href.");
console.warn(error.name, error.message);
return null;
}
const x = parseFloat(node.getAttribute("x")) || 0;
const y = parseFloat(node.getAttribute("y")) || 0;
const width = parseFloat(node.getAttribute("width"));
const height = parseFloat(node.getAttribute("height"));
const sprite = new Sprite(href, x, y);
if (!_.isNaN(width)) {
sprite.width = width;
}
if (!_.isNaN(height)) {
sprite.height = height;
}
applySvgAttributes.call(this, node, sprite, parentStyles);
return sprite;
}
};
// src/utils/xhr.js
function xhr(path, callback) {
const xhr2 = new XMLHttpRequest();
xhr2.open("GET", path);
xhr2.onreadystatechange = function() {
if (xhr2.readyState === 4 && xhr2.status === 200) {
callback(xhr2.responseText);
}
};
xhr2.send();
return xhr2;
}
// src/effects/image.js
var Image2 = class _Image extends Rectangle {
/**
* @name Two.Image#_flagTexture
* @private
* @property {Boolean} - Determines whether the {@link Two.Image#texture} needs updating.
*/
_flagTexture = false;
/**
* @name Two.Image#_flagMode
* @private
* @property {Boolean} - Determines whether the {@link Two.Image#mode} needs updating.
*/
_flagMode = false;
/**
* @name Two.Image#_texture
* @private
* @see {@link Two.Image#texture}
*/
_texture = null;
/**
* @name Two.Image#_mode
* @private
* @see {@link Two.Image#mode}
*/
_mode = "fill";
constructor(src, ox, oy, width, height, mode) {
super(ox, oy, width || 1, height || 1);
this._renderer.type = "image";
for (let prop in proto24) {
Object.defineProperty(this, prop, proto24[prop]);
}
this.noStroke();
this.noFill();
if (src instanceof Texture) {
this.texture = src;
} else if (typeof src === "string") {
this.texture = new Texture(src);
}
if (typeof mode === "string") {
this.mode = mode;
}
this._update();
}
/**
* @name Two.Image.Modes
* @property {Object} Modes - Different mode types to render an image inspired by Figma.
* @property {String} Modes.fill - Scale image to fill the bounds while preserving aspect ratio.
* @property {String} Modes.fit - Scale image to fit within bounds while preserving aspect ratio.
* @property {String} Modes.crop - Scale image to fill bounds while preserving aspect ratio, cropping excess.
* @property {String} Modes.tile - Repeat image at original size to fill the bounds.
* @property {String} Modes.stretch - Stretch image to fill dimensions, ignoring aspect ratio.
*/
static Modes = {
fill: "fill",
fit: "fit",
crop: "crop",
tile: "tile",
stretch: "stretch"
};
/**
* @name Two.Image.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Image}.
*/
static Properties = ["texture", "mode"];
/**
* @name Two.Image.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Image} to create a new instance
* @returns {Two.Image}
* @description Create a new {@link Two.Image} from an object notation of a {@link Two.Image}.
* @nota-bene Works in conjunction with {@link Two.Image#toObject}
*/
static fromObject(obj) {
const image = new _Image().copy(obj);
if ("id" in obj) {
image.id = obj.id;
}
return image;
}
/**
* @name Two.Image#copy
* @function
* @param {Two.Image} image - The reference {@link Two.Image}
* @description Copy the properties of one {@link Two.Image} onto another.
*/
copy(image) {
super.copy.call(this, image);
for (let i = 0; i < _Image.Properties.length; i++) {
const k = _Image.Properties[i];
if (k in image) {
this[k] = image[k];
}
}
return this;
}
/**
* @name Two.Image#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Image}
* @description Create a new instance of {@link Two.Image} with the same properties of the current image.
*/
clone(parent) {
const clone = new _Image(
this.texture,
this.translation.x,
this.translation.y,
this.width,
this.height
);
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Image#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the image.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "image";
object.texture = this.texture.toObject();
object.mode = this.mode;
return object;
}
/**
* @name Two.Image#dispose
* @function
* @returns {Two.Image}
* @description Release the image's renderer resources and detach all events.
* This method disposes the texture (calling dispose() for thorough cleanup) and inherits comprehensive
* cleanup from the Rectangle/Path hierarchy while preserving the renderer type
* for potential re-attachment.
*/
dispose() {
super.dispose();
if (this._texture && typeof this._texture.dispose === "function") {
this._texture.dispose();
} else if (this._texture && typeof this._texture.unbind === "function") {
this._texture.unbind();
}
return this;
}
/**
* @name Two.Image#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
const effect = this._texture;
if (effect) {
if (this._flagTexture) {
this.fill = effect;
}
if (effect.loaded) {
const iw = effect.image.width;
const ih = effect.image.height;
const rw = this.width;
const rh = this.height;
const scaleX = rw / iw;
const scaleY = rh / ih;
switch (this._mode) {
case _Image.Modes.fill: {
const scale = Math.max(scaleX, scaleY);
effect.scale = scale;
effect.offset.x = 0;
effect.offset.y = 0;
effect.repeat = "repeat";
break;
}
case _Image.Modes.fit: {
const scale = Math.min(scaleX, scaleY);
effect.scale = scale;
effect.offset.x = 0;
effect.offset.y = 0;
effect.repeat = "no-repeat";
break;
}
case _Image.Modes.crop: {
break;
}
case _Image.Modes.tile: {
effect.offset.x = (iw - rw) / 2;
effect.offset.y = (ih - rh) / 2;
effect.repeat = "repeat";
break;
}
case _Image.Modes.stretch:
default: {
effect.scale = new Vector(scaleX, scaleY);
effect.offset.x = 0;
effect.offset.y = 0;
effect.repeat = "repeat";
}
}
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Image#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
super.flagReset.call(this);
this._flagTexture = this._flagMode = false;
return this;
}
};
var proto24 = {
texture: {
enumerable: true,
get: function() {
return this._texture;
},
set: function(v) {
this._texture = v;
this._flagTexture = true;
}
},
mode: {
enumerable: true,
get: function() {
return this._mode;
},
set: function(v) {
this._mode = v;
this._flagMode = true;
}
}
};
// src/renderers/canvas.js
var emptyArray = [];
var max4 = Math.max;
var min4 = Math.min;
var abs = Math.abs;
var sin6 = Math.sin;
var cos6 = Math.cos;
var acos = Math.acos;
var sqrt = Math.sqrt;
var canvas2 = {
isHidden: /(undefined|none|transparent)/i,
alignments: {
left: "start",
middle: "center",
right: "end"
},
baselines: {
top: "top",
middle: "middle",
bottom: "bottom",
baseline: "alphabetic"
},
getRendererType: function(type) {
return type in canvas2 ? type : "path";
},
group: {
renderChild: function(child) {
const prop = canvas2.getRendererType(child._renderer.type);
canvas2[prop].render.call(child, this.ctx, true, this.clip);
},
render: function(ctx) {
if (!this._visible) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const matrix = this._matrix.elements;
const parent = this.parent;
this._renderer.opacity = this._opacity * (parent && parent._renderer ? parent._renderer.opacity : 1);
const mask = this._mask;
const defaultMatrix = isDefaultMatrix(matrix);
const shouldIsolate = !defaultMatrix || !!mask;
if (!this._renderer.context) {
this._renderer.context = {};
}
this._renderer.context.ctx = ctx;
if (shouldIsolate) {
ctx.save();
if (!defaultMatrix) {
ctx.transform(
matrix[0],
matrix[3],
matrix[1],
matrix[4],
matrix[2],
matrix[5]
);
}
}
if (mask) {
const prop = canvas2.getRendererType(mask._renderer.type);
canvas2[prop].render.call(mask, ctx, true);
}
if (this._opacity > 0 && this._scale !== 0) {
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
const prop = canvas2.getRendererType(child._renderer.type);
canvas2[prop].render.call(child, ctx);
}
}
if (shouldIsolate) {
ctx.restore();
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
path: {
render: function(ctx, forced, parentClipped) {
let matrix, stroke, linewidth, fill, opacity, visible, cap, join, miter, closed2, commands, length, last, prev, a, b, c, d, ux, uy, vx, vy, ar, bl, br, cl, x, y, mask, clip, defaultMatrix, isOffset, dashes, po;
po = this.parent && this.parent._renderer ? this.parent._renderer.opacity : 1;
mask = this._mask;
clip = this._clip;
opacity = this._opacity * (po || 1);
visible = this._visible;
if (!forced && (!visible || clip || opacity === 0)) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
matrix = this._matrix.elements;
stroke = this._stroke;
linewidth = this._linewidth;
fill = this._fill;
cap = this._cap;
join = this._join;
miter = this._miter;
closed2 = this._closed;
commands = this._renderer.vertices;
length = commands.length;
last = length - 1;
defaultMatrix = isDefaultMatrix(matrix);
dashes = this.dashes;
if (!defaultMatrix) {
ctx.save();
ctx.transform(
matrix[0],
matrix[3],
matrix[1],
matrix[4],
matrix[2],
matrix[5]
);
}
if (mask) {
const prop = canvas2.getRendererType(mask._renderer.type);
canvas2[prop].render.call(mask, ctx, true);
}
if (fill) {
if (typeof fill === "string") {
ctx.fillStyle = fill;
} else {
const prop = canvas2.getRendererType(fill._renderer.type);
canvas2[prop].render.call(fill, ctx, this);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === "string") {
ctx.strokeStyle = stroke;
} else {
const prop = canvas2.getRendererType(stroke._renderer.type);
canvas2[prop].render.call(stroke, ctx, this);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(this);
}
if (miter) {
ctx.miterLimit = miter;
}
if (join) {
ctx.lineJoin = join;
}
if (!closed2 && cap) {
ctx.lineCap = cap;
}
}
if (typeof opacity === "number") {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
ctx.beginPath();
let rx, ry, xAxisRotation, largeArcFlag, sweepFlag, ax, ay;
for (let i = 0; i < length; i++) {
b = commands[i];
x = b.x;
y = b.y;
switch (b.command) {
case Commands.close:
ctx.closePath();
break;
case Commands.arc:
rx = b.rx;
ry = b.ry;
xAxisRotation = b.xAxisRotation;
largeArcFlag = b.largeArcFlag;
sweepFlag = b.sweepFlag;
prev = closed2 ? mod(i - 1, length) : max4(i - 1, 0);
a = commands[prev];
ax = a.x;
ay = a.y;
canvas2.renderSvgArcCommand(
ctx,
ax,
ay,
rx,
ry,
largeArcFlag,
sweepFlag,
xAxisRotation,
x,
y
);
break;
case Commands.curve:
prev = closed2 ? mod(i - 1, length) : Math.max(i - 1, 0);
a = commands[prev];
ar = a.controls && a.controls.right || Vector.zero;
bl = b.controls && b.controls.left || Vector.zero;
if (a._relative) {
vx = ar.x + a.x;
vy = ar.y + a.y;
} else {
vx = ar.x;
vy = ar.y;
}
if (b._relative) {
ux = bl.x + b.x;
uy = bl.y + b.y;
} else {
ux = bl.x;
uy = bl.y;
}
ctx.bezierCurveTo(vx, vy, ux, uy, x, y);
if (i >= last && closed2) {
c = d;
br = b.controls && b.controls.right || Vector.zero;
cl = c.controls && c.controls.left || Vector.zero;
if (b._relative) {
vx = br.x + b.x;
vy = br.y + b.y;
} else {
vx = br.x;
vy = br.y;
}
if (c._relative) {
ux = cl.x + c.x;
uy = cl.y + c.y;
} else {
ux = cl.x;
uy = cl.y;
}
x = c.x;
y = c.y;
ctx.bezierCurveTo(vx, vy, ux, uy, x, y);
}
break;
case Commands.line:
ctx.lineTo(x, y);
break;
case Commands.move:
d = b;
ctx.moveTo(x, y);
break;
}
}
if (closed2) {
ctx.closePath();
}
if (!clip && !parentClipped) {
if (!canvas2.isHidden.test(fill)) {
isOffset = fill._renderer && fill._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(fill._renderer.scale.x, fill._renderer.scale.y);
}
ctx.fill();
if (isOffset) {
ctx.restore();
}
}
if (!canvas2.isHidden.test(stroke)) {
isOffset = stroke._renderer && stroke._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(
-stroke._renderer.offset.x,
-stroke._renderer.offset.y
);
ctx.scale(stroke._renderer.scale.x, stroke._renderer.scale.y);
ctx.lineWidth = linewidth / stroke._renderer.scale.x;
}
ctx.stroke();
if (isOffset) {
ctx.restore();
}
}
}
if (!defaultMatrix) {
ctx.restore();
}
if (clip && !parentClipped) {
ctx.clip();
}
if (dashes && dashes.length > 0) {
ctx.setLineDash(emptyArray);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
points: {
render: function(ctx, forced, parentClipped) {
let me, stroke, linewidth, fill, opacity, visible, size, commands, length, b, x, y, defaultMatrix, isOffset, dashes, po;
po = this.parent && this.parent._renderer ? this.parent._renderer.opacity : 1;
opacity = this._opacity * (po || 1);
visible = this._visible;
if (!forced && (!visible || opacity === 0)) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
me = this._matrix.elements;
stroke = this._stroke;
linewidth = this._linewidth;
fill = this._fill;
commands = this._renderer.collection;
length = commands.length;
defaultMatrix = isDefaultMatrix(me);
dashes = this.dashes;
size = this._size;
if (!defaultMatrix) {
ctx.save();
ctx.transform(me[0], me[3], me[1], me[4], me[2], me[5]);
}
if (fill) {
if (typeof fill === "string") {
ctx.fillStyle = fill;
} else {
const prop = canvas2.getRendererType(fill._renderer.type);
canvas2[prop].render.call(fill, ctx, this);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === "string") {
ctx.strokeStyle = stroke;
} else {
const prop = canvas2.getRendererType(stroke._renderer.type);
canvas2[prop].render.call(stroke, ctx, this);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(this);
}
}
if (typeof opacity === "number") {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
ctx.beginPath();
let radius = size * 0.5, m;
if (!this._sizeAttenuation) {
m = this.worldMatrix.elements;
m = decomposeMatrix(m[0], m[3], m[1], m[4], m[2], m[5]);
radius /= Math.max(m.scaleX, m.scaleY);
}
for (let i = 0; i < length; i++) {
b = commands[i];
x = b.x;
y = b.y;
ctx.moveTo(x + radius, y);
ctx.arc(x, y, radius, 0, TWO_PI);
}
if (!parentClipped) {
if (!canvas2.isHidden.test(fill)) {
isOffset = fill._renderer && fill._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(fill._renderer.scale.x, fill._renderer.scale.y);
}
ctx.fill();
if (isOffset) {
ctx.restore();
}
}
if (!canvas2.isHidden.test(stroke)) {
isOffset = stroke._renderer && stroke._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(
-stroke._renderer.offset.x,
-stroke._renderer.offset.y
);
ctx.scale(stroke._renderer.scale.x, stroke._renderer.scale.y);
ctx.lineWidth = linewidth / stroke._renderer.scale.x;
}
ctx.stroke();
if (isOffset) {
ctx.restore();
}
}
}
if (!defaultMatrix) {
ctx.restore();
}
if (dashes && dashes.length > 0) {
ctx.setLineDash(emptyArray);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
text: {
render: function(ctx, forced, parentClipped) {
const po = this.parent && this.parent._renderer ? this.parent._renderer.opacity : 1;
const opacity = this._opacity * po;
const visible = this._visible;
const mask = this._mask;
const clip = this._clip;
if (!forced && (!visible || clip || opacity === 0)) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const matrix = this._matrix.elements;
const stroke = this._stroke;
const linewidth = this._linewidth;
const fill = this._fill;
const decoration = this._decoration;
const direction = this._direction;
const defaultMatrix = isDefaultMatrix(matrix);
const isOffset = fill._renderer && fill._renderer.offset && stroke._renderer && stroke._renderer.offset;
const dashes = this.dashes;
const alignment = canvas2.alignments[this._alignment] || this._alignment;
const baseline = canvas2.baselines[this._baseline] || this._baseline;
let a, b, c, d, e, sx, sy, x1, y1, x2, y2;
if (!defaultMatrix) {
ctx.save();
ctx.transform(
matrix[0],
matrix[3],
matrix[1],
matrix[4],
matrix[2],
matrix[5]
);
}
if (mask) {
const prop = canvas2.getRendererType(mask._renderer.type);
canvas2[prop].render.call(mask, ctx, true);
}
if (!isOffset) {
ctx.font = [
this._style,
this._weight,
this._size + "px/" + this._leading + "px",
this._family
].join(" ");
}
ctx.textAlign = alignment;
ctx.textBaseline = baseline;
ctx.direction = direction;
if (fill) {
if (typeof fill === "string") {
ctx.fillStyle = fill;
} else {
const prop = canvas2.getRendererType(fill._renderer.type);
canvas2[prop].render.call(fill, ctx, this);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === "string") {
ctx.strokeStyle = stroke;
} else {
const prop = canvas2.getRendererType(stroke._renderer.type);
canvas2[prop].render.call(stroke, ctx, this);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(this);
}
}
if (typeof opacity === "number") {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
if (!clip && !parentClipped) {
if (!canvas2.isHidden.test(fill)) {
if (fill._renderer && fill._renderer.offset) {
sx = fill._renderer.scale.x;
sy = fill._renderer.scale.y;
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(sx, sy);
a = this._size / fill._renderer.scale.y;
b = this._leading / fill._renderer.scale.y;
ctx.font = [
this._style,
this._weight,
a + "px/",
b + "px",
this._family
].join(" ");
c = fill._renderer.offset.x / fill._renderer.scale.x;
d = fill._renderer.offset.y / fill._renderer.scale.y;
ctx.fillText(this.value, c, d);
ctx.restore();
} else {
ctx.fillText(this.value, 0, 0);
}
}
if (!canvas2.isHidden.test(stroke)) {
if (stroke._renderer && stroke._renderer.offset) {
sx = stroke._renderer.scale.x;
sy = stroke._renderer.scale.y;
ctx.save();
ctx.translate(
-stroke._renderer.offset.x,
-stroke._renderer.offset.y
);
ctx.scale(sx, sy);
a = this._size / stroke._renderer.scale.y;
b = this._leading / stroke._renderer.scale.y;
ctx.font = [
this._style,
this._weight,
a + "px/",
b + "px",
this._family
].join(" ");
c = stroke._renderer.offset.x / stroke._renderer.scale.x;
d = stroke._renderer.offset.y / stroke._renderer.scale.y;
e = linewidth / stroke._renderer.scale.x;
ctx.lineWidth = e;
ctx.strokeText(this.value, c, d);
ctx.restore();
} else {
ctx.strokeText(this.value, 0, 0);
}
}
}
if (/(underline|strikethrough)/i.test(decoration)) {
const metrics = ctx.measureText(this.value);
let scalar = 1;
switch (decoration) {
case "underline":
y1 = metrics.actualBoundingBoxDescent;
y2 = metrics.actualBoundingBoxDescent;
break;
case "strikethrough":
y1 = 0;
y2 = 0;
scalar = 0.5;
break;
}
switch (baseline) {
case "top":
y1 += this._size * scalar;
y2 += this._size * scalar;
break;
case "baseline":
case "bottom":
y1 -= this._size * scalar;
y2 -= this._size * scalar;
break;
}
switch (alignment) {
case "left":
case "start":
x1 = 0;
x2 = metrics.width;
break;
case "right":
case "end":
x1 = -metrics.width;
x2 = 0;
break;
default:
x1 = -metrics.width / 2;
x2 = metrics.width / 2;
}
ctx.lineWidth = Math.max(Math.floor(this._size / 15), 1);
ctx.strokeStyle = ctx.fillStyle;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
if (!defaultMatrix) {
ctx.restore();
}
if (clip && !parentClipped) {
ctx.clip();
}
if (dashes && dashes.length > 0) {
ctx.setLineDash(emptyArray);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
"linear-gradient": {
render: function(ctx, parent) {
if (!parent) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
if (!this._renderer.effect || this._flagEndPoints || this._flagStops || this._flagUnits) {
let rect;
let lx = this.left._x;
let ly = this.left._y;
let rx = this.right._x;
let ry = this.right._y;
if (/objectBoundingBox/i.test(this._units)) {
rect = parent.getBoundingClientRect(true);
lx = (lx - 0.5) * rect.width;
ly = (ly - 0.5) * rect.height;
rx = (rx - 0.5) * rect.width;
ry = (ry - 0.5) * rect.height;
}
this._renderer.effect = ctx.createLinearGradient(lx, ly, rx, ry);
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
this._renderer.effect.addColorStop(stop._offset, stop._color);
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
"radial-gradient": {
render: function(ctx, parent) {
if (!parent) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
if (!this._renderer.effect || this._flagCenter || this._flagFocal || this._flagRadius || this._flagStops || this._flagUnits) {
let rect;
let cx = this.center._x;
let cy = this.center._y;
let fx = this.focal._x;
let fy = this.focal._y;
let radius = this._radius;
if (/objectBoundingBox/i.test(this._units)) {
rect = parent.getBoundingClientRect(true);
cx = (cx - 0.5) * rect.width * 0.5;
cy = (cy - 0.5) * rect.height * 0.5;
fx = (fx - 0.5) * rect.width * 0.5;
fy = (fy - 0.5) * rect.height * 0.5;
radius *= Math.min(rect.width, rect.height);
}
this._renderer.effect = ctx.createRadialGradient(
cx,
cy,
0,
fx,
fy,
radius
);
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
this._renderer.effect.addColorStop(stop._offset, stop._color);
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
texture: {
render: function(ctx) {
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const image = this.image;
if (!this._renderer.effect || (this._flagLoaded || this._flagImage || this._flagVideo || this._flagRepeat) && this.loaded) {
this._renderer.effect = ctx.createPattern(this.image, this._repeat);
}
if (this._flagOffset || this._flagLoaded || this._flagScale) {
if (!(this._renderer.offset instanceof Vector)) {
this._renderer.offset = new Vector();
}
this._renderer.offset.x = -this._offset.x;
this._renderer.offset.y = -this._offset.y;
if (image) {
this._renderer.offset.x += image.width / 2;
this._renderer.offset.y += image.height / 2;
if (this._scale instanceof Vector) {
this._renderer.offset.x *= this._scale.x;
this._renderer.offset.y *= this._scale.y;
} else {
this._renderer.offset.x *= this._scale;
this._renderer.offset.y *= this._scale;
}
}
}
if (this._flagScale || this._flagLoaded) {
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
if (this._scale instanceof Vector) {
this._renderer.scale.copy(this._scale);
} else {
this._renderer.scale.set(this._scale, this._scale);
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
renderSvgArcCommand: function(ctx, ax, ay, rx, ry, largeArcFlag, sweepFlag, xAxisRotation, x, y) {
xAxisRotation = xAxisRotation * Math.PI / 180;
rx = abs(rx);
ry = abs(ry);
const dx2 = (ax - x) / 2;
const dy2 = (ay - y) / 2;
const x1p = cos6(xAxisRotation) * dx2 + sin6(xAxisRotation) * dy2;
const y1p = -sin6(xAxisRotation) * dx2 + cos6(xAxisRotation) * dy2;
const x1ps = x1p * x1p;
const y1ps = y1p * y1p;
let rxs = rx * rx;
let rys = ry * ry;
const cr = x1ps / rxs + y1ps / rys;
if (cr > 1) {
const s = sqrt(cr);
rx = s * rx;
ry = s * ry;
rxs = rx * rx;
rys = ry * ry;
}
const dq = rxs * y1ps + rys * x1ps;
const pq = (rxs * rys - dq) / dq;
let q = sqrt(max4(0, pq));
if (largeArcFlag === sweepFlag) q = -q;
const cxp = q * rx * y1p / ry;
const cyp = -q * ry * x1p / rx;
const cx = cos6(xAxisRotation) * cxp - sin6(xAxisRotation) * cyp + (ax + x) / 2;
const cy = sin6(xAxisRotation) * cxp + cos6(xAxisRotation) * cyp + (ay + y) / 2;
const startAngle = svgAngle2(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
const delta = svgAngle2(
(x1p - cxp) / rx,
(y1p - cyp) / ry,
(-x1p - cxp) / rx,
(-y1p - cyp) / ry
) % TWO_PI;
const endAngle = startAngle + delta;
const clockwise = sweepFlag === 0;
renderArcEstimate(
ctx,
cx,
cy,
rx,
ry,
startAngle,
endAngle,
clockwise,
xAxisRotation
);
}
};
var Renderer = class extends Events {
constructor(params) {
super();
const smoothing = params.smoothing !== false;
this.domElement = params.domElement || document.createElement("canvas");
this.ctx = this.domElement.getContext("2d");
this.overdraw = params.overdraw || false;
if (typeof this.ctx.imageSmoothingEnabled !== "undefined") {
this.ctx.imageSmoothingEnabled = smoothing;
}
this.scene = new Group();
this.scene.parent = this;
}
/**
* @name Two.CanvasRenderer.Utils
* @property {Object} - A massive object filled with utility functions and properties to render Two.js objects to a ``.
*/
static Utils = canvas2;
/**
* @name Two.CanvasRenderer#setSize
* @function
* @fires resize
* @param {Number} width - The new width of the renderer.
* @param {Number} height - The new height of the renderer.
* @param {Number} [ratio] - The new pixel ratio (pixel density) of the renderer. Defaults to calculate the pixel density of the user's screen.
* @description Change the size of the renderer.
*/
setSize(width, height, ratio) {
this.width = width;
this.height = height;
this.ratio = typeof ratio === "undefined" ? getRatio(this.ctx) : ratio;
this.domElement.width = width * this.ratio;
this.domElement.height = height * this.ratio;
if (this.domElement.style) {
_.extend(this.domElement.style, {
width: width + "px",
height: height + "px"
});
}
return this.trigger(Events.Types.resize, width, height, ratio);
}
/**
* @name Two.CanvasRenderer#render
* @function
* @description Render the current scene to the ``.
*/
render() {
const isOne = this.ratio === 1;
if (!isOne) {
this.ctx.save();
this.ctx.scale(this.ratio, this.ratio);
}
if (!this.overdraw) {
this.ctx.clearRect(0, 0, this.width, this.height);
}
canvas2.group.render.call(this.scene, this.ctx);
if (!isOne) {
this.ctx.restore();
}
return this;
}
};
function renderArcEstimate(ctx, ox, oy, rx, ry, startAngle, endAngle, clockwise, xAxisRotation) {
const delta = endAngle - startAngle;
const epsilon = Curve.Tolerance.epsilon;
const samePoints = Math.abs(delta) < epsilon;
let deltaAngle = mod(delta, TWO_PI);
if (deltaAngle < epsilon) {
if (samePoints) {
deltaAngle = 0;
} else {
deltaAngle = TWO_PI;
}
}
if (clockwise === true && !samePoints) {
if (deltaAngle === TWO_PI) {
deltaAngle = -TWO_PI;
} else {
deltaAngle = deltaAngle - TWO_PI;
}
}
for (let i = 0; i < Constants.Resolution; i++) {
const t = i / (Constants.Resolution - 1);
const angle = startAngle + t * deltaAngle;
let x = ox + rx * Math.cos(angle);
let y = oy + ry * Math.sin(angle);
if (xAxisRotation !== 0) {
const cos7 = Math.cos(xAxisRotation);
const sin7 = Math.sin(xAxisRotation);
const tx = x - ox;
const ty = y - oy;
x = tx * cos7 - ty * sin7 + ox;
y = tx * sin7 + ty * cos7 + oy;
}
ctx.lineTo(x, y);
}
}
function svgAngle2(ux, uy, vx, vy) {
const dot = ux * vx + uy * vy;
const len = sqrt(ux * ux + uy * uy) * sqrt(vx * vx + vy * vy);
let ang = acos(max4(-1, min4(1, dot / len)));
if (ux * vy - uy * vx < 0) {
ang = -ang;
}
return ang;
}
function isDefaultMatrix(m) {
return m[0] === 1 && m[3] === 0 && m[1] === 0 && m[4] === 1 && m[2] === 0 && m[5] === 0;
}
// src/renderers/svg.js
var svg = {
version: 1.1,
ns: "http://www.w3.org/2000/svg",
xlink: "http://www.w3.org/1999/xlink",
alignments: {
left: "start",
center: "middle",
right: "end"
},
baselines: {
top: "hanging",
middle: "middle",
bottom: "ideographic",
baseline: "alphabetic"
},
// Create an svg namespaced element.
createElement: function(name, attrs) {
const tag = name;
const elem = document.createElementNS(svg.ns, tag);
if (tag === "svg") {
attrs = _.defaults(attrs || {}, {
version: svg.version
});
}
if (attrs && Object.keys(attrs).length > 0) {
svg.setAttributes(elem, attrs);
}
return elem;
},
// Add attributes from an svg element.
setAttributes: function(elem, attrs) {
const keys = Object.keys(attrs);
for (let i = 0; i < keys.length; i++) {
if (/href/.test(keys[i])) {
elem.setAttributeNS(svg.xlink, keys[i], attrs[keys[i]]);
} else {
elem.setAttribute(keys[i], attrs[keys[i]]);
}
}
return this;
},
// Remove attributes from an svg element.
removeAttributes: function(elem, attrs) {
for (let key in attrs) {
elem.removeAttribute(key);
}
return this;
},
// Turn a set of vertices into a string for the d property of a path
// element. It is imperative that the string collation is as fast as
// possible, because this call will be happening multiple times a
// second.
toString: function(points, closed2) {
let l = points.length, last = l - 1, d, string = "";
for (let i = 0; i < l; i++) {
const b = points[i];
const prev = closed2 ? mod(i - 1, l) : Math.max(i - 1, 0);
const a = points[prev];
let command, c;
let vx, vy, ux, uy, ar, bl, br, cl;
let rx, ry, xAxisRotation, largeArcFlag, sweepFlag;
let x = toFixed(b.x);
let y = toFixed(b.y);
switch (b.command) {
case Commands.close:
command = Commands.close;
break;
case Commands.arc:
rx = b.rx;
ry = b.ry;
xAxisRotation = b.xAxisRotation;
largeArcFlag = b.largeArcFlag;
sweepFlag = b.sweepFlag;
command = Commands.arc + " " + rx + " " + ry + " " + xAxisRotation + " " + largeArcFlag + " " + sweepFlag + " " + x + " " + y;
break;
case Commands.curve:
ar = a.controls && a.controls.right || Vector.zero;
bl = b.controls && b.controls.left || Vector.zero;
if (a.relative) {
vx = toFixed(ar.x + a.x);
vy = toFixed(ar.y + a.y);
} else {
vx = toFixed(ar.x);
vy = toFixed(ar.y);
}
if (b.relative) {
ux = toFixed(bl.x + b.x);
uy = toFixed(bl.y + b.y);
} else {
ux = toFixed(bl.x);
uy = toFixed(bl.y);
}
command = (i === 0 ? Commands.move : Commands.curve) + " " + vx + " " + vy + " " + ux + " " + uy + " " + x + " " + y;
break;
case Commands.move:
d = b;
command = Commands.move + " " + x + " " + y;
break;
default:
command = b.command + " " + x + " " + y;
}
if (i >= last && closed2) {
if (b.command === Commands.curve) {
c = d;
br = b.controls && b.controls.right || b;
cl = c.controls && c.controls.left || c;
if (b.relative) {
vx = toFixed(br.x + b.x);
vy = toFixed(br.y + b.y);
} else {
vx = toFixed(br.x);
vy = toFixed(br.y);
}
if (c.relative) {
ux = toFixed(cl.x + c.x);
uy = toFixed(cl.y + c.y);
} else {
ux = toFixed(cl.x);
uy = toFixed(cl.y);
}
x = toFixed(c.x);
y = toFixed(c.y);
command += " C " + vx + " " + vy + " " + ux + " " + uy + " " + x + " " + y;
}
if (b.command !== Commands.close) {
command += " Z";
}
}
string += command + " ";
}
return string;
},
pointsToString: function(points, size) {
let string = "";
const r = size * 0.5;
for (let i = 0; i < points.length; i++) {
const x = points[i].x;
const y = points[i].y - r;
string += Commands.move + " " + x + " " + y + " ";
string += "a " + r + " " + r + " 0 1 0 0.001 0 Z";
}
return string;
},
getClip: function(shape, domElement) {
let clip = shape._renderer.clip;
if (!clip) {
clip = shape._renderer.clip = svg.createElement("clipPath", {
"clip-rule": "nonzero"
});
}
if (clip.parentNode === null) {
domElement.defs.appendChild(clip);
}
return clip;
},
getRendererType: function(type) {
return type in svg ? type : "path";
},
defs: {
update: function(domElement) {
const { defs } = domElement;
if (defs._flagUpdate) {
const children = Array.prototype.slice.call(defs.children, 0);
for (let i = 0; i < children.length; i++) {
const child = children[i];
const id = child.id;
const selector = `[fill="url(#${id})"],[stroke="url(#${id})"],[clip-path="url(#${id})"]`;
const exists = domElement.querySelector(selector);
if (!exists) {
defs.removeChild(child);
}
}
defs._flagUpdate = false;
}
}
},
group: {
// TODO: Can speed up.
// TODO: How does this effect a f
appendChild: function(object) {
const elem = object._renderer.elem;
if (!elem) {
return;
}
const tag = elem.nodeName;
if (!tag || /(radial|linear)gradient/i.test(tag) || object._clip) {
return;
}
this.elem.appendChild(elem);
},
removeChild: function(object) {
const elem = object._renderer.elem;
if (!elem || elem.parentNode != this.elem) {
return;
}
const tag = elem.nodeName;
if (!tag) {
return;
}
if (object._clip) {
return;
}
this.elem.removeChild(elem);
},
orderChild: function(object) {
this.elem.appendChild(object._renderer.elem);
},
renderChild: function(child) {
const prop = svg.getRendererType(child._renderer.type);
svg[prop].render.call(child, this);
},
render: function(domElement) {
if (!this._visible && !this._flagVisible || this._opacity === 0 && !this._flagOpacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
if (!this._renderer.elem) {
this._renderer.elem = svg.createElement("g", {
id: this.id
});
domElement.appendChild(this._renderer.elem);
}
const flagMatrix = this._matrix.manual || this._flagMatrix;
const context = {
domElement,
elem: this._renderer.elem
};
if (flagMatrix) {
this._renderer.elem.setAttribute(
"transform",
"matrix(" + this._matrix.toString() + ")"
);
}
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
const prop = svg.getRendererType(child._renderer.type);
svg[prop].render.call(child, domElement);
}
if (this._flagId) {
this._renderer.elem.setAttribute("id", this._id);
}
if (this._flagOpacity) {
this._renderer.elem.setAttribute("opacity", this._opacity);
}
if (this._flagVisible) {
this._renderer.elem.setAttribute(
"display",
this._visible ? "inline" : "none"
);
}
if (this._flagClassName) {
this._renderer.elem.setAttribute("class", this.classList.join(" "));
}
if (this._flagAdditions) {
this.additions.forEach(svg.group.appendChild, context);
}
if (this._flagSubtractions) {
this.subtractions.forEach(svg.group.removeChild, context);
}
if (this._flagOrder) {
this.children.forEach(svg.group.orderChild, context);
}
if (this._flagMask) {
if (this._mask) {
const prop = svg.getRendererType(this._mask._renderer.type);
svg[prop].render.call(this._mask, domElement);
this._renderer.elem.setAttribute(
"clip-path",
"url(#" + this._mask.id + ")"
);
} else {
this._renderer.elem.removeAttribute("clip-path");
}
}
if (this.dataset) {
Object.assign(this._renderer.elem.dataset, this.dataset);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
path: {
render: function(domElement) {
if (this._opacity === 0 && !this._flagOpacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const changed = {};
const flagMatrix = this._matrix.manual || this._flagMatrix;
if (flagMatrix) {
changed.transform = "matrix(" + this._matrix.toString() + ")";
}
if (this._flagId) {
changed.id = this._id;
}
if (this._flagVertices) {
const vertices = svg.toString(this._renderer.vertices, this._closed);
changed.d = vertices;
}
if (this._fill && this._fill._renderer) {
this._renderer.hasFillEffect = true;
this._fill._update();
const prop = svg.getRendererType(this._fill._renderer.type);
svg[prop].render.call(this._fill, domElement, true);
}
if (this._flagFill) {
changed.fill = this._fill && this._fill.id ? "url(#" + this._fill.id + ")" : this._fill;
if (this._renderer.hasFillEffect && typeof this._fill.id === "undefined") {
domElement.defs._flagUpdate = true;
delete this._renderer.hasFillEffect;
}
}
if (this._stroke && this._stroke._renderer) {
this._renderer.hasStrokeEffect = true;
this._stroke._update();
const prop = svg.getRendererType(this._stroke._renderer.type);
svg[prop].render.call(this._stroke, domElement, true);
}
if (this._flagStroke) {
changed.stroke = this._stroke && this._stroke.id ? "url(#" + this._stroke.id + ")" : this._stroke;
if (this._renderer.hasStrokeEffect && typeof this._stroke.id === "undefined") {
domElement.defs._flagUpdate = true;
delete this._renderer.hasStrokeEffect;
}
}
if (this._flagLinewidth) {
changed["stroke-width"] = getEffectiveStrokeWidth(this);
}
if (this._flagOpacity) {
changed["stroke-opacity"] = this._opacity;
changed["fill-opacity"] = this._opacity;
}
if (this._flagClassName) {
changed["class"] = this.classList.join(" ");
}
if (this._flagVisible) {
changed.visibility = this._visible ? "visible" : "hidden";
}
if (this._flagCap) {
changed["stroke-linecap"] = this._cap;
}
if (this._flagJoin) {
changed["stroke-linejoin"] = this._join;
}
if (this._flagMiter) {
changed["stroke-miterlimit"] = this._miter;
}
if (this.dashes && this.dashes.length > 0) {
changed["stroke-dasharray"] = this.dashes.join(" ");
changed["stroke-dashoffset"] = this.dashes.offset || 0;
}
if (!this._renderer.elem) {
changed.id = this._id;
this._renderer.elem = svg.createElement("path", changed);
domElement.appendChild(this._renderer.elem);
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._flagClip) {
const clip = svg.getClip(this, domElement);
const elem = this._renderer.elem;
if (this._clip) {
elem.removeAttribute("id");
clip.setAttribute("id", this.id);
clip.appendChild(elem);
} else {
clip.removeAttribute("id");
elem.setAttribute("id", this.id);
this.parent._renderer.elem.appendChild(elem);
}
}
if (this._flagMask) {
if (this._mask) {
const prop = svg.getRendererType(this._mask._renderer.type);
svg[prop].render.call(this._mask, domElement);
this._renderer.elem.setAttribute(
"clip-path",
"url(#" + this._mask.id + ")"
);
} else {
this._renderer.elem.removeAttribute("clip-path");
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
points: {
render: function(domElement) {
if (this._opacity === 0 && !this._flagOpacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const changed = {};
const flagMatrix = this._matrix.manual || this._flagMatrix;
if (flagMatrix) {
changed.transform = "matrix(" + this._matrix.toString() + ")";
}
if (this._flagId) {
changed.id = this._id;
}
if (this._flagVertices || this._flagSize || this._flagSizeAttenuation) {
let size = this._size;
if (!this._sizeAttenuation) {
const me = this.worldMatrix.elements;
const m = decomposeMatrix(me[0], me[3], me[1], me[4], me[2], me[5]);
size /= Math.max(m.scaleX, m.scaleY);
}
const vertices = svg.pointsToString(this._renderer.collection, size);
changed.d = vertices;
}
if (this._fill && this._fill._renderer) {
this._renderer.hasFillEffect = true;
this._fill._update();
const prop = svg.getRendererType(this._fill._renderer.type);
svg[prop].render.call(this._fill, domElement, true);
}
if (this._flagFill) {
changed.fill = this._fill && this._fill.id ? "url(#" + this._fill.id + ")" : this._fill;
if (this._renderer.hasFillEffect && typeof this._fill.id === "undefined") {
domElement.defs._flagUpdate = true;
delete this._renderer.hasFillEffect;
}
}
if (this._stroke && this._stroke._renderer) {
this._renderer.hasStrokeEffect = true;
this._stroke._update();
const prop = svg.getRendererType(this._stroke._renderer.type);
svg[prop].render.call(this._stroke, domElement, true);
}
if (this._flagStroke) {
changed.stroke = this._stroke && this._stroke.id ? "url(#" + this._stroke.id + ")" : this._stroke;
if (this._renderer.hasStrokeEffect && typeof this._stroke.id === "undefined") {
domElement.defs._flagUpdate = true;
delete this._renderer.hasStrokeEffect;
}
}
if (this._flagLinewidth) {
changed["stroke-width"] = getEffectiveStrokeWidth(this);
}
if (this._flagOpacity) {
changed["stroke-opacity"] = this._opacity;
changed["fill-opacity"] = this._opacity;
}
if (this._flagClassName) {
changed["class"] = this.classList.join(" ");
}
if (this._flagVisible) {
changed.visibility = this._visible ? "visible" : "hidden";
}
if (this.dashes && this.dashes.length > 0) {
changed["stroke-dasharray"] = this.dashes.join(" ");
changed["stroke-dashoffset"] = this.dashes.offset || 0;
}
if (!this._renderer.elem) {
changed.id = this._id;
this._renderer.elem = svg.createElement("path", changed);
domElement.appendChild(this._renderer.elem);
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
text: {
render: function(domElement) {
this._update();
const changed = {};
const flagMatrix = this._matrix.manual || this._flagMatrix;
if (flagMatrix) {
changed.transform = "matrix(" + this._matrix.toString() + ")";
}
if (this._flagId) {
changed.id = this._id;
}
if (this._flagFamily) {
changed["font-family"] = this._family;
}
if (this._flagSize) {
changed["font-size"] = this._size;
}
if (this._flagLeading) {
changed["line-height"] = this._leading;
}
if (this._flagAlignment) {
changed["text-anchor"] = svg.alignments[this._alignment] || this._alignment;
}
if (this._flagBaseline) {
changed["dominant-baseline"] = svg.baselines[this._baseline] || this._baseline;
}
if (this._flagStyle) {
changed["font-style"] = this._style;
}
if (this._flagWeight) {
changed["font-weight"] = this._weight;
}
if (this._flagDecoration) {
changed["text-decoration"] = this._decoration;
}
if (this._flagDirection) {
changed["direction"] = this._direction;
}
if (this._fill && this._fill._renderer) {
this._renderer.hasFillEffect = true;
this._fill._update();
const prop = svg.getRendererType(this._fill._renderer.type);
svg[prop].render.call(this._fill, domElement, true);
}
if (this._flagFill) {
changed.fill = this._fill && this._fill.id ? "url(#" + this._fill.id + ")" : this._fill;
if (this._renderer.hasFillEffect && typeof this._fill.id === "undefined") {
domElement.defs._flagUpdate = true;
delete this._renderer.hasFillEffect;
}
}
if (this._stroke && this._stroke._renderer) {
this._renderer.hasStrokeEffect = true;
this._stroke._update();
const prop = svg.getRendererType(this._stroke._renderer.type);
svg[prop].render.call(this._stroke, domElement, true);
}
if (this._flagStroke) {
changed.stroke = this._stroke && this._stroke.id ? "url(#" + this._stroke.id + ")" : this._stroke;
if (this._renderer.hasStrokeEffect && typeof this._stroke.id === "undefined") {
domElement.defs._flagUpdate = true;
delete this._renderer.hasStrokeEffect;
}
}
if (this._flagLinewidth) {
changed["stroke-width"] = getEffectiveStrokeWidth(this);
}
if (this._flagOpacity) {
changed.opacity = this._opacity;
}
if (this._flagClassName) {
changed["class"] = this.classList.join(" ");
}
if (this._flagVisible) {
changed.visibility = this._visible ? "visible" : "hidden";
}
if (this.dashes && this.dashes.length > 0) {
changed["stroke-dasharray"] = this.dashes.join(" ");
changed["stroke-dashoffset"] = this.dashes.offset || 0;
}
if (!this._renderer.elem) {
changed.id = this._id;
this._renderer.elem = svg.createElement("text", changed);
domElement.appendChild(this._renderer.elem);
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._flagClip) {
const clip = svg.getClip(this, domElement);
const elem = this._renderer.elem;
if (this._clip) {
elem.removeAttribute("id");
clip.setAttribute("id", this.id);
clip.appendChild(elem);
} else {
clip.removeAttribute("id");
elem.setAttribute("id", this.id);
this.parent._renderer.elem.appendChild(elem);
}
}
if (this._flagMask) {
if (this._mask) {
const prop = svg.getRendererType(this._mask._renderer.type);
svg[prop].render.call(this._mask, domElement);
this._renderer.elem.setAttribute(
"clip-path",
"url(#" + this._mask.id + ")"
);
} else {
this._renderer.elem.removeAttribute("clip-path");
}
}
if (this._flagValue) {
this._renderer.elem.textContent = this._value;
}
return this.flagReset();
}
},
"linear-gradient": {
render: function(domElement, silent) {
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
if (!silent) {
this._update();
}
const changed = {};
if (this._flagId) {
changed.id = this._id;
}
if (this._flagEndPoints) {
changed.x1 = this.left._x;
changed.y1 = this.left._y;
changed.x2 = this.right._x;
changed.y2 = this.right._y;
}
if (this._flagSpread) {
changed.spreadMethod = this._spread;
}
if (this._flagUnits) {
changed.gradientUnits = this._units;
}
if (!this._renderer.elem) {
changed.id = this._id;
this._renderer.elem = svg.createElement("linearGradient", changed);
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._renderer.elem.parentNode === null) {
domElement.defs.appendChild(this._renderer.elem);
}
if (this._flagStops) {
const lengthChanged = this._renderer.elem.childNodes.length !== this.stops.length;
if (lengthChanged) {
while (this._renderer.elem.lastChild) {
this._renderer.elem.removeChild(this._renderer.elem.lastChild);
}
}
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
const attrs = {};
if (stop._flagOffset) {
attrs.offset = 100 * stop._offset + "%";
}
if (stop._flagColor) {
attrs["stop-color"] = stop._color;
}
if (stop._flagOpacity) {
attrs["stop-opacity"] = stop._opacity;
}
if (!stop._renderer.elem) {
stop._renderer.elem = svg.createElement("stop", attrs);
} else {
svg.setAttributes(stop._renderer.elem, attrs);
}
if (lengthChanged) {
this._renderer.elem.appendChild(stop._renderer.elem);
}
stop.flagReset();
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
"radial-gradient": {
render: function(domElement, silent) {
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
if (!silent) {
this._update();
}
const changed = {};
if (this._flagId) {
changed.id = this._id;
}
if (this._flagCenter) {
changed.cx = this.center._x;
changed.cy = this.center._y;
}
if (this._flagFocal) {
changed.fx = this.focal._x;
changed.fy = this.focal._y;
}
if (this._flagRadius) {
changed.r = this._radius;
}
if (this._flagSpread) {
changed.spreadMethod = this._spread;
}
if (this._flagUnits) {
changed.gradientUnits = this._units;
}
if (!this._renderer.elem) {
changed.id = this._id;
this._renderer.elem = svg.createElement("radialGradient", changed);
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._renderer.elem.parentNode === null) {
domElement.defs.appendChild(this._renderer.elem);
}
if (this._flagStops) {
const lengthChanged = this._renderer.elem.childNodes.length !== this.stops.length;
if (lengthChanged) {
while (this._renderer.elem.lastChild) {
this._renderer.elem.removeChild(this._renderer.elem.lastChild);
}
}
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
const attrs = {};
if (stop._flagOffset) {
attrs.offset = 100 * stop._offset + "%";
}
if (stop._flagColor) {
attrs["stop-color"] = stop._color;
}
if (stop._flagOpacity) {
attrs["stop-opacity"] = stop._opacity;
}
if (!stop._renderer.elem) {
stop._renderer.elem = svg.createElement("stop", attrs);
} else {
svg.setAttributes(stop._renderer.elem, attrs);
}
if (lengthChanged) {
this._renderer.elem.appendChild(stop._renderer.elem);
}
stop.flagReset();
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
texture: {
render: function(domElement, silent) {
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
if (!silent) {
this._update();
}
const changed = {};
const styles = { x: 0, y: 0 };
const image = this.image;
if (this._flagId) {
changed.id = this._id;
}
if (this._flagLoaded && this.loaded) {
switch (image.nodeName.toLowerCase()) {
case "canvas":
styles.href = styles["xlink:href"] = image.toDataURL("image/png");
break;
case "img":
case "image":
styles.href = styles["xlink:href"] = this.src;
break;
}
}
if (this._flagOffset || this._flagLoaded || this._flagScale) {
changed.x = this._offset.x;
changed.y = this._offset.y;
if (image) {
changed.x -= image.width / 2;
changed.y -= image.height / 2;
if (this._scale instanceof Vector) {
changed.x *= this._scale.x;
changed.y *= this._scale.y;
} else {
changed.x *= this._scale;
changed.y *= this._scale;
}
}
if (changed.x > 0) {
changed.x *= -1;
}
if (changed.y > 0) {
changed.y *= -1;
}
}
if (this._flagScale || this._flagLoaded || this._flagRepeat) {
changed.width = 0;
changed.height = 0;
if (image) {
changed.width = image.width;
changed.height = image.height;
switch (this._repeat) {
case "no-repeat":
changed.width += 1;
changed.height += 1;
break;
}
if (this._scale instanceof Vector) {
changed.width *= this._scale.x;
changed.height *= this._scale.y;
} else {
changed.width *= this._scale;
changed.height *= this._scale;
}
if (/no-repeat/i.test(this._repeat)) {
styles.preserveAspectRatio = "xMidYMid";
} else {
styles.preserveAspectRatio = "none";
}
styles.width = changed.width;
styles.height = changed.height;
}
}
if (this._flagScale || this._flagLoaded) {
if (!this._renderer.image) {
this._renderer.image = svg.createElement("image", styles);
} else {
svg.setAttributes(this._renderer.image, styles);
}
}
if (!this._renderer.elem) {
changed.id = this._id;
changed.patternUnits = "userSpaceOnUse";
this._renderer.elem = svg.createElement("pattern", changed);
} else if (Object.keys(changed).length !== 0) {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._renderer.elem.parentNode === null) {
domElement.defs.appendChild(this._renderer.elem);
}
if (this._renderer.elem && this._renderer.image && !this._renderer.appended) {
this._renderer.elem.appendChild(this._renderer.image);
this._renderer.appended = true;
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
}
};
var Renderer2 = class extends Events {
constructor(params) {
super();
this.domElement = params.domElement || svg.createElement("svg");
this.scene = new Group();
this.scene.parent = this;
this.defs = svg.createElement("defs");
this.defs._flagUpdate = false;
this.domElement.appendChild(this.defs);
this.domElement.defs = this.defs;
this.domElement.style.overflow = "hidden";
}
/**
* @name Two.SVGRenderer.Utils
* @property {Object} - A massive object filled with utility functions and properties to render Two.js objects to a ``.
*/
static Utils = svg;
/**
* @name Two.SVGRenderer#setSize
* @function
* @param {Number} width - The new width of the renderer.
* @param {Number} height - The new height of the renderer.
* @description Change the size of the renderer.
* @nota-bene Triggers a `Two.Events.resize`.
*/
setSize(width, height) {
this.width = width;
this.height = height;
svg.setAttributes(this.domElement, {
width,
height
});
return this.trigger(Events.Types.resize, width, height);
}
/**
* @name Two.SVGRenderer#render
* @function
* @description Render the current scene to the ``.
*/
render() {
svg.group.render.call(this.scene, this.domElement);
svg.defs.update(this.domElement);
return this;
}
};
// src/utils/shaders.js
var shaders = {
create: function(gl, source, type) {
const shader = gl.createShader(gl[type]);
gl.shaderSource(shader, source);
gl.compileShader(shader);
const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
const error = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new TwoError("unable to compile shader " + shader + ": " + error);
}
return shader;
},
types: {
vertex: "VERTEX_SHADER",
fragment: "FRAGMENT_SHADER"
},
path: {
vertex: `
precision mediump float;
attribute vec2 a_position;
uniform mat3 u_matrix;
uniform vec2 u_resolution;
uniform vec4 u_rect;
varying vec2 v_textureCoords;
void main() {
vec2 rectCoords = (a_position * (u_rect.zw - u_rect.xy)) + u_rect.xy;
vec2 projected = (u_matrix * vec3(rectCoords, 1.0)).xy;
vec2 normal = projected / u_resolution;
vec2 clipspace = (normal * 2.0) - 1.0;
gl_Position = vec4(clipspace * vec2(1.0, -1.0), 0.0, 1.0);
v_textureCoords = a_position;
}
`,
fragment: `
precision mediump float;
uniform sampler2D u_image;
varying vec2 v_textureCoords;
void main() {
vec4 texel = texture2D(u_image, v_textureCoords);
if (texel.a == 0.0) {
discard;
}
gl_FragColor = texel;
}
`
},
points: {
vertex: `
precision mediump float;
attribute vec2 a_position;
uniform float u_size;
uniform mat3 u_matrix;
uniform vec2 u_resolution;
varying vec2 v_textureCoords;
void main() {
vec2 projected = (u_matrix * vec3(a_position, 1.0)).xy;
vec2 normal = projected / u_resolution;
vec2 clipspace = (normal * 2.0) - 1.0;
gl_PointSize = u_size;
gl_Position = vec4(clipspace * vec2(1.0, -1.0), 0.0, 1.0);
v_textureCoords = a_position;
}
`,
fragment: `
precision mediump float;
uniform sampler2D u_image;
void main() {
vec4 texel = texture2D(u_image, gl_PointCoord);
if (texel.a == 0.0) {
discard;
}
gl_FragColor = texel;
}
`
}
};
// src/renderers/webgl.js
var multiplyMatrix = Matrix2.Multiply;
var identity = [1, 0, 0, 0, 1, 0, 0, 0, 1];
var transformation = new NumArray(9);
var CanvasUtils = Renderer.Utils;
var vector2 = new Vector();
var quad = new NumArray([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]);
var webgl = {
precision: 0.9,
isHidden: /(undefined|none|transparent)/i,
canvas: root.document ? root.document.createElement("canvas") : { getContext: function() {
} },
alignments: {
left: "start",
middle: "center",
right: "end"
},
matrix: new Matrix2(),
group: {
removeChild: function(child, gl) {
if (child.children) {
for (let i = 0; i < child.children.length; i++) {
webgl.group.removeChild(child.children[i], gl);
}
}
if (child._renderer.texture) {
gl.deleteTexture(child._renderer.texture);
delete child._renderer.texture;
}
if (child._renderer.positionBuffer) {
gl.deleteBuffer(child._renderer.positionBuffer);
delete child._renderer.positionBuffer;
}
},
/**
* @function
// * @type {(gl: any, programs: any) => any}
* @param {WebGLContext} gl
* @param {Object} programs
*/
render: function(gl, programs) {
if (!this._visible) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const parent = this.parent;
const flagParentMatrix = parent._matrix && parent._matrix.manual || parent._flagMatrix;
const flagMatrix = this._matrix.manual || this._flagMatrix;
if (flagParentMatrix || flagMatrix) {
if (!this._renderer.matrix) {
this._renderer.matrix = new NumArray(9);
}
this._matrix.toTransformArray(true, transformation);
multiplyMatrix(
transformation,
parent._renderer.matrix,
this._renderer.matrix
);
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
if (this._scale instanceof Vector) {
this._renderer.scale.x = this._scale.x;
this._renderer.scale.y = this._scale.y;
} else {
this._renderer.scale.x = this._scale;
this._renderer.scale.y = this._scale;
}
if (!/renderer/i.test(parent._renderer.type)) {
this._renderer.scale.x *= parent._renderer.scale.x;
this._renderer.scale.y *= parent._renderer.scale.y;
}
if (flagParentMatrix) {
this._flagMatrix = true;
}
}
if (this._mask) {
gl.clear(gl.STENCIL_BUFFER_BIT);
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.ALWAYS, 1, 0);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
gl.colorMask(false, false, false, false);
const prop = Renderer.Utils.getRendererType(
this._mask._renderer.type
);
webgl[prop].render.call(this._mask, gl, programs, this);
gl.stencilFunc(gl.EQUAL, 1, 255);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
gl.colorMask(true, true, true, true);
}
this._flagOpacity = parent._flagOpacity || this._flagOpacity;
this._renderer.opacity = this._opacity * (parent && parent._renderer ? parent._renderer.opacity : 1);
let i;
if (this._flagSubtractions) {
for (i = 0; i < this.subtractions.length; i++) {
webgl.group.removeChild(this.subtractions[i], gl);
}
}
for (i = 0; i < this.children.length; i++) {
const child = this.children[i];
const prop = Renderer.Utils.getRendererType(child._renderer.type);
webgl[prop].render.call(child, gl, programs);
}
if (this._mask) {
gl.disable(gl.STENCIL_TEST);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
path: {
updateCanvas: function(gl, elem) {
let prev, a, c, ux, uy, vx, vy, ar, bl, br, cl, x, y;
let isOffset;
const commands = elem._renderer.vertices;
const canvas3 = this.canvas;
const ctx = this.ctx;
const ratio = gl.renderer.ratio;
const scale = vector2.copy(elem._renderer.scale).multiply(ratio);
const stroke = elem._stroke;
const linewidth = elem._linewidth;
const fill = elem._fill;
const opacity = elem._renderer.opacity || elem._opacity;
const cap = elem._cap;
const join = elem._join;
const miter = elem._miter;
const closed2 = elem._closed;
const dashes = elem.dashes;
const length = commands.length;
const last = length - 1;
canvas3.width = Math.max(
Math.ceil(elem._renderer.rect.width * scale.x),
1
);
canvas3.height = Math.max(
Math.ceil(elem._renderer.rect.height * scale.y),
1
);
const centroid = elem._renderer.rect.centroid;
const cx = centroid.x;
const cy = centroid.y;
ctx.clearRect(0, 0, canvas3.width, canvas3.height);
if (fill) {
if (typeof fill === "string") {
ctx.fillStyle = fill;
} else {
const prop = Renderer.Utils.getRendererType(
fill._renderer.type
);
webgl[prop].render.call(fill, ctx, elem);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === "string") {
ctx.strokeStyle = stroke;
} else {
const prop = Renderer.Utils.getRendererType(
stroke._renderer.type
);
webgl[prop].render.call(stroke, ctx, elem);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(elem);
}
if (miter) {
ctx.miterLimit = miter;
}
if (join) {
ctx.lineJoin = join;
}
if (!closed2 && cap) {
ctx.lineCap = cap;
}
}
if (typeof opacity === "number") {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
let d, rx, ry, xAxisRotation, largeArcFlag, sweepFlag, ax, ay;
ctx.save();
ctx.scale(scale.x, scale.y);
ctx.translate(cx, cy);
ctx.beginPath();
for (let i = 0; i < commands.length; i++) {
const b = commands[i];
x = b.x;
y = b.y;
switch (b.command) {
case Commands.close:
ctx.closePath();
break;
case Commands.arc:
rx = b.rx;
ry = b.ry;
xAxisRotation = b.xAxisRotation;
largeArcFlag = b.largeArcFlag;
sweepFlag = b.sweepFlag;
prev = closed2 ? mod(i - 1, length) : Math.max(i - 1, 0);
a = commands[prev];
ax = a.x;
ay = a.y;
CanvasUtils.renderSvgArcCommand(
ctx,
ax,
ay,
rx,
ry,
largeArcFlag,
sweepFlag,
xAxisRotation,
x,
y
);
break;
case Commands.curve:
prev = closed2 ? mod(i - 1, length) : Math.max(i - 1, 0);
a = commands[prev];
ar = a.controls && a.controls.right || Vector.zero;
bl = b.controls && b.controls.left || Vector.zero;
if (a._relative) {
vx = ar.x + a.x;
vy = ar.y + a.y;
} else {
vx = ar.x;
vy = ar.y;
}
if (b._relative) {
ux = bl.x + b.x;
uy = bl.y + b.y;
} else {
ux = bl.x;
uy = bl.y;
}
ctx.bezierCurveTo(vx, vy, ux, uy, x, y);
if (i >= last && closed2) {
c = d;
br = b.controls && b.controls.right || Vector.zero;
cl = c.controls && c.controls.left || Vector.zero;
if (b._relative) {
vx = br.x + b.x;
vy = br.y + b.y;
} else {
vx = br.x;
vy = br.y;
}
if (c._relative) {
ux = cl.x + c.x;
uy = cl.y + c.y;
} else {
ux = cl.x;
uy = cl.y;
}
x = c.x;
y = c.y;
ctx.bezierCurveTo(vx, vy, ux, uy, x, y);
}
break;
case Commands.line:
ctx.lineTo(x, y);
break;
case Commands.move:
d = b;
ctx.moveTo(x, y);
break;
}
}
if (closed2) {
ctx.closePath();
}
if (!webgl.isHidden.test(fill)) {
isOffset = fill._renderer && fill._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(fill._renderer.scale.x, fill._renderer.scale.y);
}
ctx.fill();
if (isOffset) {
ctx.restore();
}
}
if (!webgl.isHidden.test(stroke)) {
isOffset = stroke._renderer && stroke._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-stroke._renderer.offset.x, -stroke._renderer.offset.y);
ctx.scale(stroke._renderer.scale.x, stroke._renderer.scale.y);
ctx.lineWidth = linewidth / stroke._renderer.scale.x;
}
ctx.stroke();
if (isOffset) {
ctx.restore();
}
}
ctx.restore();
},
// Returns the rect of a set of verts. Typically takes vertices that are
// "centered" around 0 and returns them to be anchored upper-left.
getBoundingClientRect: function(vertices, border, rect) {
let left = Infinity, right = -Infinity, top = Infinity, bottom = -Infinity, width, height;
vertices.forEach(function(v) {
const x = v.x, y = v.y, controls = v.controls;
let a, b, c, d, cl, cr;
top = Math.min(y, top);
left = Math.min(x, left);
right = Math.max(x, right);
bottom = Math.max(y, bottom);
if (!v.controls) {
return;
}
cl = controls.left;
cr = controls.right;
if (!cl || !cr) {
return;
}
a = v._relative ? cl.x + x : cl.x;
b = v._relative ? cl.y + y : cl.y;
c = v._relative ? cr.x + x : cr.x;
d = v._relative ? cr.y + y : cr.y;
if (!a || !b || !c || !d) {
return;
}
top = Math.min(b, d, top);
left = Math.min(a, c, left);
right = Math.max(a, c, right);
bottom = Math.max(b, d, bottom);
});
if (typeof border === "number") {
top -= border;
left -= border;
right += border;
bottom += border;
}
width = right - left;
height = bottom - top;
rect.top = top;
rect.left = left;
rect.right = right;
rect.bottom = bottom;
rect.width = width;
rect.height = height;
if (!rect.centroid) {
rect.centroid = {};
}
rect.centroid.x = -left;
rect.centroid.y = -top;
},
render: function(gl, programs, forcedParent) {
if (!this._visible || !this._opacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const parent = forcedParent || this.parent;
const prop = Renderer.Utils.getRendererType(this._renderer.type);
const program = programs[prop];
const flagParentMatrix = parent._matrix.manual || parent._flagMatrix;
const flagMatrix = this._matrix.manual || this._flagMatrix;
const parentChanged = this._renderer.parent !== parent;
const flagTexture = this._flagVertices || this._flagFill || this._fill instanceof LinearGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagEndPoints) || this._fill instanceof RadialGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagRadius || this._fill._flagCenter || this._fill._flagFocal) || this._fill instanceof Texture && (this._fill._flagLoaded && this._fill.loaded || this._fill._flagImage || this._fill._flagVideo || this._fill._flagRepeat || this._fill._flagOffset || this._fill._flagScale) || this._stroke instanceof LinearGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagEndPoints) || this._stroke instanceof RadialGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagRadius || this._stroke._flagCenter || this._stroke._flagFocal) || this._stroke instanceof Texture && (this._stroke._flagLoaded && this._stroke.loaded || this._stroke._flagImage || this._stroke._flagVideo || this._stroke._flagRepeat || this._stroke._flagOffset || this._fill._flagScale) || this._flagStroke || this._flagLinewidth || this._flagOpacity || parent._flagOpacity || this._flagVisible || this._flagCap || this._flagJoin || this._flagMiter || this._flagScale || this.dashes && this.dashes.length > 0 || !this._renderer.texture;
if (flagParentMatrix || flagMatrix || parentChanged) {
if (!this._renderer.matrix) {
this._renderer.matrix = new NumArray(9);
}
this._matrix.toTransformArray(true, transformation);
multiplyMatrix(
transformation,
parent._renderer.matrix,
this._renderer.matrix
);
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
let sx, sy;
if (this._scale instanceof Vector) {
sx = this._scale.x * parent._renderer.scale.x;
sy = this._scale.y * parent._renderer.scale.y;
} else {
sx = this._scale * parent._renderer.scale.x;
sy = this._scale * parent._renderer.scale.y;
}
this._renderer.scale.x = sx < 0 ? -sx : sx;
this._renderer.scale.y = sy < 0 ? -sy : sy;
if (parentChanged) {
this._renderer.parent = parent;
}
}
if (this._mask) {
gl.clear(gl.STENCIL_BUFFER_BIT);
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.ALWAYS, 1, 0);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
gl.colorMask(false, false, false, false);
const prop2 = Renderer.Utils.getRendererType(
this._mask._renderer.type
);
webgl[prop2].render.call(this._mask, gl, programs, this);
gl.stencilFunc(gl.EQUAL, 1, 255);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
gl.colorMask(true, true, true, true);
}
if (flagTexture) {
if (!this._renderer.rect) {
this._renderer.rect = {};
}
this._renderer.opacity = this._opacity * parent._renderer.opacity;
webgl.path.getBoundingClientRect(
this._renderer.vertices,
this._linewidth,
this._renderer.rect
);
webgl.updateTexture.call(webgl, gl, this);
} else {
if (this._fill && this._fill._update) {
this._fill._update();
}
if (this._stroke && this._stroke._update) {
this._stroke._update();
}
}
if (this._clip && !forcedParent || !this._renderer.texture) {
return this;
}
if (programs.current !== program) {
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, programs.buffers.position);
gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.position);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
if (!programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, "u_resolution"),
programs.resolution.width,
programs.resolution.height
);
}
programs.current = program;
}
if (programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, "u_resolution"),
programs.resolution.width,
programs.resolution.height
);
}
gl.bindTexture(gl.TEXTURE_2D, this._renderer.texture);
const rect = this._renderer.rect;
gl.uniformMatrix3fv(program.matrix, false, this._renderer.matrix);
gl.uniform4f(program.rect, rect.left, rect.top, rect.right, rect.bottom);
gl.drawArrays(gl.TRIANGLES, 0, 6);
if (this._mask) {
gl.disable(gl.STENCIL_TEST);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
points: {
// The canvas is a texture that is a rendering of one vertex
updateCanvas: function(gl, elem) {
let isOffset;
const canvas3 = this.canvas;
const ctx = this.ctx;
const ratio = gl.renderer.ratio;
const stroke = elem._stroke;
const linewidth = elem._linewidth;
const fill = elem._fill;
const opacity = elem._renderer.opacity || elem._opacity;
const dashes = elem.dashes;
const size = elem._size * ratio;
let dimension = size;
if (!webgl.isHidden.test(stroke)) {
dimension += linewidth;
}
canvas3.width = getPoT(dimension);
canvas3.height = canvas3.width;
const aspect = dimension / canvas3.width;
const cx = canvas3.width / 2;
const cy = canvas3.height / 2;
ctx.clearRect(0, 0, canvas3.width, canvas3.height);
if (fill) {
if (typeof fill === "string") {
ctx.fillStyle = fill;
} else {
const prop = Renderer.Utils.getRendererType(
fill._renderer.type
);
webgl[prop].render.call(fill, ctx, elem);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === "string") {
ctx.strokeStyle = stroke;
} else {
const prop = Renderer.Utils.getRendererType(
stroke._renderer.type
);
webgl[prop].render.call(stroke, ctx, elem);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(elem) / aspect;
}
}
if (typeof opacity === "number") {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
ctx.save();
ctx.translate(cx, cy);
ctx.scale(webgl.precision, webgl.precision);
ctx.beginPath();
ctx.arc(0, 0, size / aspect * 0.5, 0, TWO_PI);
ctx.restore();
if (closed) {
ctx.closePath();
}
if (!webgl.isHidden.test(fill)) {
isOffset = fill._renderer && fill._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(fill._renderer.scale.x, fill._renderer.scale.y);
}
ctx.fill();
if (isOffset) {
ctx.restore();
}
}
if (!webgl.isHidden.test(stroke)) {
isOffset = stroke._renderer && stroke._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-stroke._renderer.offset.x, -stroke._renderer.offset.y);
ctx.scale(stroke._renderer.scale.x, stroke._renderer.scale.y);
ctx.lineWidth = linewidth / stroke._renderer.scale.x;
}
ctx.stroke();
if (isOffset) {
ctx.restore();
}
}
},
render: function(gl, programs, forcedParent) {
if (!this._visible || !this._opacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
let size = this._size;
const parent = forcedParent || this.parent;
const program = programs[this._renderer.type];
const sizeAttenuation = this._sizeAttenuation;
const stroke = this._stroke;
const linewidth = this._linewidth;
const flagParentMatrix = parent._matrix.manual || parent._flagMatrix;
const flagMatrix = this._matrix.manual || this._flagMatrix;
const parentChanged = this._renderer.parent !== parent;
const commands = this._renderer.vertices;
const length = this._renderer.collection.length;
const flagVertices = this._flagVertices;
const flagTexture = this._flagFill || this._fill instanceof LinearGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagEndPoints) || this._fill instanceof RadialGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagRadius || this._fill._flagCenter || this._fill._flagFocal) || this._fill instanceof Texture && (this._fill._flagLoaded && this._fill.loaded || this._fill._flagImage || this._fill._flagVideo || this._fill._flagRepeat || this._fill._flagOffset || this._fill._flagScale) || this._stroke instanceof LinearGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagEndPoints) || this._stroke instanceof RadialGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagRadius || this._stroke._flagCenter || this._stroke._flagFocal) || this._stroke instanceof Texture && (this._stroke._flagLoaded && this._stroke.loaded || this._stroke._flagImage || this._stroke._flagVideo || this._stroke._flagRepeat || this._stroke._flagOffset || this._fill._flagScale) || this._flagStroke || this._flagLinewidth || this._flagOpacity || parent._flagOpacity || this._flagVisible || this._flagScale || this.dashes && this.dashes.length > 0 || !this._renderer.texture;
if (flagParentMatrix || flagMatrix || parentChanged) {
if (!this._renderer.matrix) {
this._renderer.matrix = new NumArray(9);
}
this._matrix.toTransformArray(true, transformation);
multiplyMatrix(
transformation,
parent._renderer.matrix,
this._renderer.matrix
);
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
let sx, sy;
if (this._scale instanceof Vector) {
sx = this._scale.x * parent._renderer.scale.x;
sy = this._scale.y * parent._renderer.scale.y;
} else {
sx = this._scale * parent._renderer.scale.x;
sy = this._scale * parent._renderer.scale.y;
}
this._renderer.scale.x = sx < 0 ? -sx : sx;
this._renderer.scale.y = sy < 0 ? -sy : sy;
if (parentChanged) {
this._renderer.parent = parent;
}
}
if (flagVertices) {
const positionBuffer = this._renderer.positionBuffer;
if (positionBuffer) {
gl.deleteBuffer(positionBuffer);
}
this._renderer.positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this._renderer.positionBuffer);
gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.position);
gl.bufferData(gl.ARRAY_BUFFER, commands, gl.STATIC_DRAW);
}
if (flagTexture) {
this._renderer.opacity = this._opacity * parent._renderer.opacity;
webgl.updateTexture.call(webgl, gl, this);
} else {
if (this._fill && this._fill._update) {
this._fill._update();
}
if (this._stroke && this._stroke._update) {
this._stroke._update();
}
}
if (this._clip && !forcedParent || !this._renderer.texture) {
return this;
}
if (!webgl.isHidden.test(stroke)) {
size += linewidth;
}
size /= webgl.precision;
if (sizeAttenuation) {
size *= Math.max(this._renderer.scale.x, this._renderer.scale.y);
}
if (programs.current !== program) {
gl.useProgram(program);
if (!programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, "u_resolution"),
programs.resolution.width,
programs.resolution.height
);
}
programs.current = program;
}
if (programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, "u_resolution"),
programs.resolution.width,
programs.resolution.height
);
}
gl.bindTexture(gl.TEXTURE_2D, this._renderer.texture);
gl.uniformMatrix3fv(program.matrix, false, this._renderer.matrix);
gl.uniform1f(program.size, size * programs.resolution.ratio);
gl.drawArrays(gl.POINTS, 0, length);
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
text: {
updateCanvas: function(gl, elem) {
const canvas3 = this.canvas;
const ctx = this.ctx;
const ratio = gl.renderer.ratio;
const scale = vector2.copy(elem._renderer.scale).multiply(ratio);
const stroke = elem._stroke;
const linewidth = elem._linewidth;
const fill = elem._fill;
const opacity = elem._renderer.opacity || elem._opacity;
const dashes = elem.dashes;
const decoration = elem._decoration;
const direction = elem._direction;
canvas3.width = Math.max(
Math.ceil(elem._renderer.rect.width * scale.x),
1
);
canvas3.height = Math.max(
Math.ceil(elem._renderer.rect.height * scale.y),
1
);
const centroid = elem._renderer.rect.centroid;
const cx = centroid.x;
const cy = centroid.y;
let a, b, c, d, e, sx, sy, x1, y1, x2, y2;
const isOffset = fill._renderer && fill._renderer.offset && stroke._renderer && stroke._renderer.offset;
ctx.clearRect(0, 0, canvas3.width, canvas3.height);
if (!isOffset) {
ctx.font = [
elem._style,
elem._weight,
elem._size + "px/" + elem._leading + "px",
elem._family
].join(" ");
}
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.textDirection = direction;
if (fill) {
if (typeof fill === "string") {
ctx.fillStyle = fill;
} else {
const prop = Renderer.Utils.getRendererType(
fill._renderer.type
);
webgl[prop].render.call(fill, ctx, elem);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === "string") {
ctx.strokeStyle = stroke;
} else {
const prop = Renderer.Utils.getRendererType(
stroke._renderer.type
);
webgl[prop].render.call(stroke, ctx, elem);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(elem);
}
}
if (typeof opacity === "number") {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
ctx.save();
ctx.scale(scale.x, scale.y);
ctx.translate(cx, cy);
if (!webgl.isHidden.test(fill)) {
if (fill._renderer && fill._renderer.offset) {
sx = fill._renderer.scale.x;
sy = fill._renderer.scale.y;
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(sx, sy);
a = elem._size / fill._renderer.scale.y;
b = elem._leading / fill._renderer.scale.y;
ctx.font = [
elem._style,
elem._weight,
a + "px/",
b + "px",
elem._family
].join(" ");
c = fill._renderer.offset.x / fill._renderer.scale.x;
d = fill._renderer.offset.y / fill._renderer.scale.y;
ctx.fillText(elem.value, c, d);
ctx.restore();
} else {
ctx.fillText(elem.value, 0, 0);
}
}
if (!webgl.isHidden.test(stroke)) {
if (stroke._renderer && stroke._renderer.offset) {
sx = stroke._renderer.scale.x;
sy = stroke._renderer.scale.y;
ctx.save();
ctx.translate(-stroke._renderer.offset.x, -stroke._renderer.offset.y);
ctx.scale(sx, sy);
a = elem._size / stroke._renderer.scale.y;
b = elem._leading / stroke._renderer.scale.y;
ctx.font = [
elem._style,
elem._weight,
a + "px/",
b + "px",
elem._family
].join(" ");
c = stroke._renderer.offset.x / stroke._renderer.scale.x;
d = stroke._renderer.offset.y / stroke._renderer.scale.y;
e = linewidth / stroke._renderer.scale.x;
ctx.lineWidth = e;
ctx.strokeText(elem.value, c, d);
ctx.restore();
} else {
ctx.strokeText(elem.value, 0, 0);
}
}
if (/(underline|strikethrough)/i.test(decoration)) {
const metrics = ctx.measureText(elem.value);
switch (decoration) {
case "underline":
y1 = metrics.actualBoundingBoxDescent;
y2 = metrics.actualBoundingBoxDescent;
break;
case "strikethrough":
y1 = 0;
y2 = 0;
break;
}
x1 = -metrics.width / 2;
x2 = metrics.width / 2;
ctx.lineWidth = Math.max(Math.floor(elem._size / 15), 1);
ctx.strokeStyle = ctx.fillStyle;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
ctx.restore();
},
getBoundingClientRect: function(elem, rect) {
const ctx = webgl.ctx;
ctx.font = [
elem._style,
elem._weight,
elem._size + "px/" + elem._leading + "px",
elem._family
].join(" ");
ctx.textAlign = "center";
ctx.textBaseline = Renderer.Utils.baselines[elem._baseline] || elem._baseline;
const metrics = ctx.measureText(elem._value);
let width = metrics.width;
let height = 1.15 * (metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent);
if (this._linewidth && !webgl.isHidden.test(this._stroke)) {
width += this._linewidth * 2;
height += this._linewidth * 2;
}
const w = width / 2;
const h = height / 2;
switch (webgl.alignments[elem._alignment] || elem._alignment) {
case webgl.alignments.left:
if (elem.direction === "ltr") {
rect.left = 0;
rect.right = width;
} else {
rect.left = -width;
rect.right = 0;
}
break;
case webgl.alignments.right:
if (elem.direction === "ltr") {
rect.left = -width;
rect.right = 0;
} else {
rect.left = 0;
rect.right = width;
}
break;
default:
rect.left = -w;
rect.right = w;
}
switch (elem._baseline) {
case "bottom":
rect.top = -height;
rect.bottom = 0;
break;
case "top":
rect.top = 0;
rect.bottom = height;
break;
case "baseline":
rect.top = -h * 1.5;
rect.bottom = h * 0.5;
break;
default:
rect.top = -h;
rect.bottom = h;
}
rect.width = width;
rect.height = height;
if (!rect.centroid) {
rect.centroid = {};
}
rect.centroid.x = w;
rect.centroid.y = h;
},
render: function(gl, programs, forcedParent) {
if (!this._visible || !this._opacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const parent = forcedParent || this.parent;
const program = programs[this._renderer.type];
const flagParentMatrix = parent._matrix.manual || parent._flagMatrix;
const flagMatrix = this._matrix.manual || this._flagMatrix;
const parentChanged = this._renderer.parent !== parent;
const flagTexture = this._flagVertices || this._flagFill || this._fill instanceof LinearGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagEndPoints) || this._fill instanceof RadialGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagRadius || this._fill._flagCenter || this._fill._flagFocal) || this._fill instanceof Texture && (this._fill._flagLoaded && this._fill.loaded || this._fill._flagImage || this._fill._flagVideo || this._fill._flagRepeat || this._fill._flagOffset || this._fill._flagScale) || this._stroke instanceof LinearGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagEndPoints) || this._stroke instanceof RadialGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagRadius || this._stroke._flagCenter || this._stroke._flagFocal) || this._stroke instanceof Texture && (this._stroke._flagLoaded && this._stroke.loaded || this._stroke._flagImage || this._stroke._flagVideo || this._stroke._flagRepeat || this._stroke._flagOffset || this._fill._flagScale) || this._flagStroke || this._flagLinewidth || this._flagOpacity || parent._flagOpacity || this._flagVisible || this._flagScale || this._flagValue || this._flagFamily || this._flagSize || this._flagLeading || this._flagAlignment || this._flagBaseline || this._flagStyle || this._flagWeight || this._flagDecoration || this.dashes && this.dashes.length > 0 || !this._renderer.texture;
if (flagParentMatrix || flagMatrix || parentChanged) {
if (!this._renderer.matrix) {
this._renderer.matrix = new NumArray(9);
}
this._matrix.toTransformArray(true, transformation);
multiplyMatrix(
transformation,
parent._renderer.matrix,
this._renderer.matrix
);
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
let sx, sy;
if (this._scale instanceof Vector) {
sx = this._scale.x * parent._renderer.scale.x;
sy = this._scale.y * parent._renderer.scale.y;
} else {
sx = this._scale * parent._renderer.scale.x;
sy = this._scale * parent._renderer.scale.y;
}
this._renderer.scale.x = sx < 0 ? -sx : sx;
this._renderer.scale.y = sy < 0 ? -sy : sy;
if (parentChanged) {
this._renderer.parent = parent;
}
}
if (this._mask) {
gl.clear(gl.STENCIL_BUFFER_BIT);
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.ALWAYS, 1, 0);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
gl.colorMask(false, false, false, false);
const prop = Renderer.Utils.getRendererType(
this._mask._renderer.type
);
webgl[prop].render.call(this._mask, gl, programs, this);
gl.stencilFunc(gl.EQUAL, 1, 255);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
gl.colorMask(true, true, true, true);
}
if (flagTexture) {
if (!this._renderer.rect) {
this._renderer.rect = {};
}
this._renderer.opacity = this._opacity * parent._renderer.opacity;
webgl.text.getBoundingClientRect(this, this._renderer.rect);
webgl.updateTexture.call(webgl, gl, this);
} else {
if (this._fill && this._fill._update) {
this._fill._update();
}
if (this._stroke && this._stroke._update) {
this._stroke._update();
}
}
if (this._clip && !forcedParent || !this._renderer.texture) {
return this;
}
if (programs.current !== program) {
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, programs.buffers.position);
gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.position);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
if (!programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, "u_resolution"),
programs.resolution.width,
programs.resolution.height
);
}
programs.current = program;
}
if (programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, "u_resolution"),
programs.resolution.width,
programs.resolution.height
);
}
gl.bindTexture(gl.TEXTURE_2D, this._renderer.texture);
const rect = this._renderer.rect;
gl.uniformMatrix3fv(program.matrix, false, this._renderer.matrix);
gl.uniform4f(program.rect, rect.left, rect.top, rect.right, rect.bottom);
gl.drawArrays(gl.TRIANGLES, 0, 6);
if (this._mask) {
gl.disable(gl.STENCIL_TEST);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
"linear-gradient": {
render: function(ctx, parent) {
if (!ctx.canvas.getContext("2d") || !parent) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
if (!this._renderer.effect || this._flagEndPoints || this._flagStops || this._flagUnits) {
let rect;
let lx = this.left._x;
let ly = this.left._y;
let rx = this.right._x;
let ry = this.right._y;
if (/objectBoundingBox/i.test(this._units)) {
rect = parent.getBoundingClientRect(true);
lx = (lx - 0.5) * rect.width;
ly = (ly - 0.5) * rect.height;
rx = (rx - 0.5) * rect.width;
ry = (ry - 0.5) * rect.height;
}
this._renderer.effect = ctx.createLinearGradient(lx, ly, rx, ry);
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
this._renderer.effect.addColorStop(stop._offset, stop._color);
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
"radial-gradient": {
render: function(ctx, parent) {
if (!ctx.canvas.getContext("2d") || !parent) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
if (!this._renderer.effect || this._flagCenter || this._flagFocal || this._flagRadius || this._flagStops || this._flagUnits) {
let rect;
let cx = this.center._x;
let cy = this.center._y;
let fx = this.focal._x;
let fy = this.focal._y;
let radius = this._radius;
if (/objectBoundingBox/i.test(this._units)) {
rect = parent.getBoundingClientRect(true);
cx = (cx - 0.5) * rect.width * 0.5;
cy = (cy - 0.5) * rect.height * 0.5;
fx = (fx - 0.5) * rect.width * 0.5;
fy = (fy - 0.5) * rect.height * 0.5;
radius *= Math.min(rect.width, rect.height);
}
this._renderer.effect = ctx.createRadialGradient(
cx,
cy,
0,
fx,
fy,
radius
);
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
this._renderer.effect.addColorStop(stop._offset, stop._color);
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
texture: {
render: function(ctx, elem) {
if (!ctx.canvas.getContext("2d")) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const image = this.image;
if ((this._flagLoaded || this._flagImage || this._flagVideo || this._flagRepeat) && this.loaded) {
this._renderer.effect = ctx.createPattern(image, this._repeat);
} else if (!this._renderer.effect) {
return this.flagReset();
}
if (this._flagOffset || this._flagLoaded || this._flagScale) {
if (!(this._renderer.offset instanceof Vector)) {
this._renderer.offset = new Vector();
}
this._renderer.offset.x = -this._offset.x;
this._renderer.offset.y = -this._offset.y;
if (image) {
this._renderer.offset.x += image.width / 2;
this._renderer.offset.y += image.height / 2;
if (this._scale instanceof Vector) {
this._renderer.offset.x *= this._scale.x;
this._renderer.offset.y *= this._scale.y;
} else {
this._renderer.offset.x *= this._scale;
this._renderer.offset.y *= this._scale;
}
}
}
if (this._flagScale || this._flagLoaded) {
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
let sx, sy;
if (this._scale instanceof Vector) {
sx = this._scale.x;
sy = this._scale.y;
} else {
sx = this._scale;
sy = this._scale;
}
this._renderer.scale.x = sx < 0 ? -sx : sx;
this._renderer.scale.y = sy < 0 ? -sy : sy;
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
updateTexture: function(gl, elem) {
const prop = Renderer.Utils.getRendererType(elem._renderer.type);
this[prop].updateCanvas.call(webgl, gl, elem);
if (this.canvas.width <= 0 || this.canvas.height <= 0) {
if (elem._renderer.texture) {
gl.deleteTexture(elem._renderer.texture);
}
delete elem._renderer.texture;
return;
}
if (!elem._renderer.texture) {
elem._renderer.texture = gl.createTexture();
}
gl.bindTexture(gl.TEXTURE_2D, elem._renderer.texture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
this.canvas
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
},
program: {
create: function(gl, shaders2) {
let program, linked, error;
program = gl.createProgram();
_.each(shaders2, function(s) {
gl.attachShader(program, s);
});
gl.linkProgram(program);
linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
error = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw new TwoError("unable to link program: " + error);
}
return program;
}
},
extensions: {
init: function(gl) {
const extensions = {};
const names = [
"EXT_texture_filter_anisotropic",
"WEBGL_compressed_texture_s3tc",
"OES_texture_float_linear",
"WEBGL_multisampled_render_to_texture"
];
for (let i = 0; i < names.length; i++) {
const name = names[i];
extensions[name] = webgl.extensions.get(gl, name);
}
return extensions;
},
get: function(gl, name) {
return gl.getExtension(name) || gl.getExtension(`MOZ_${name}`) || gl.getExtension(`WEBKIT_${name}`);
}
},
TextureRegistry: new Registry()
};
webgl.ctx = webgl.canvas.getContext("2d");
var Renderer3 = class extends Events {
constructor(params) {
super();
let gl, program, vs, fs;
this.domElement = params.domElement || document.createElement("canvas");
if (typeof params.offscreenElement !== "undefined") {
webgl.canvas = params.offscreenElement;
webgl.ctx = webgl.canvas.getContext("2d");
}
this.scene = new Group();
this.scene.parent = this;
this._renderer = {
type: "renderer",
matrix: new NumArray(identity),
scale: 1,
opacity: 1
};
this._flagMatrix = true;
params = _.defaults(params || {}, {
antialias: false,
alpha: true,
premultipliedAlpha: true,
stencil: true,
preserveDrawingBuffer: true,
overdraw: false
});
this.overdraw = params.overdraw;
gl = this.ctx = this.domElement.getContext("webgl", params) || this.domElement.getContext("experimental-webgl", params);
if (!this.ctx) {
throw new TwoError(
"unable to create a webgl context. Try using another renderer."
);
}
vs = shaders.create(gl, shaders.path.vertex, shaders.types.vertex);
fs = shaders.create(gl, shaders.path.fragment, shaders.types.fragment);
this.programs = {
current: null,
buffers: {
position: gl.createBuffer()
},
resolution: {
width: 0,
height: 0,
ratio: 1,
flagged: false
}
};
program = this.programs.path = webgl.program.create(gl, [vs, fs]);
this.programs.text = this.programs.path;
gl.extensions = webgl.extensions.init(gl);
gl.renderer = this;
program.position = gl.getAttribLocation(program, "a_position");
program.matrix = gl.getUniformLocation(program, "u_matrix");
program.rect = gl.getUniformLocation(program, "u_rect");
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.position);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
vs = shaders.create(gl, shaders.points.vertex, shaders.types.vertex);
fs = shaders.create(gl, shaders.points.fragment, shaders.types.fragment);
program = this.programs.points = webgl.program.create(gl, [vs, fs]);
program.position = gl.getAttribLocation(program, "a_position");
program.matrix = gl.getUniformLocation(program, "u_matrix");
program.size = gl.getUniformLocation(program, "u_size");
gl.enable(gl.BLEND);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
}
/**
* @name Two.WebGLRenderer.Utils
* @property {Object} - A massive object filled with utility functions and properties to render Two.js objects to a `` through the WebGL API.
*/
static Utils = webgl;
/**
* @name Two.WebGLRenderer#setSize
* @function
* @fires resize
* @param {Number} width - The new width of the renderer.
* @param {Number} height - The new height of the renderer.
* @param {Number} [ratio] - The new pixel ratio (pixel density) of the renderer. Defaults to calculate the pixel density of the user's screen.
* @description Change the size of the renderer.
*/
setSize(width, height, ratio) {
let w, h;
const ctx = this.ctx;
this.width = width;
this.height = height;
this.ratio = typeof ratio === "undefined" ? getRatio(ctx) : ratio;
this.domElement.width = width * this.ratio;
this.domElement.height = height * this.ratio;
if (_.isObject(this.domElement.style)) {
_.extend(this.domElement.style, {
width: width + "px",
height: height + "px"
});
}
this._renderer.matrix[0] = this._renderer.matrix[4] = this._renderer.scale = this.ratio;
this._flagMatrix = true;
w = width * this.ratio;
h = height * this.ratio;
ctx.viewport(0, 0, w, h);
this.programs.resolution.width = w;
this.programs.resolution.height = h;
this.programs.resolution.ratio = this.ratio;
this.programs.resolution.flagged = true;
return this.trigger(Events.Types.resize, width, height, ratio);
}
/**
* @name Two.WebGLRenderer#render
* @function
* @description Render the current scene to the ``.
*/
render() {
const gl = this.ctx;
if (!this.overdraw) {
gl.clear(gl.COLOR_BUFFER_BIT);
}
webgl.group.render.call(this.scene, gl, this.programs);
this._flagMatrix = false;
this.programs.resolution.flagged = true;
return this;
}
};
// src/two.js
var Utils = _.extend(
{
Error: TwoError,
getRatio,
read,
xhr
},
_,
CanvasPolyfill,
curves_exports,
math_exports
);
var Two = class _Two {
// Warning: inherit events while overriding static properties
/**
* @private
*/
_events = new Events();
// Getters and setters aren't enumerable
get _bound() {
return this._events._bound;
}
set _bound(v) {
this._events._bound = v;
}
addEventListener() {
return this._events.addEventListener?.apply(this, arguments);
}
on() {
return this._events.addEventListener?.apply(this, arguments);
}
bind() {
return this._events.addEventListener?.apply(this, arguments);
}
removeEventListener() {
return this._events.removeEventListener?.apply(this, arguments);
}
off() {
return this._events.removeEventListener?.apply(this, arguments);
}
unbind() {
return this._events.removeEventListener?.apply(this, arguments);
}
dispatchEvent() {
return this._events.dispatchEvent?.apply(this, arguments);
}
trigger() {
return this._events.dispatchEvent?.apply(this, arguments);
}
listen() {
return this._events.listen?.apply(this, arguments);
}
ignore() {
return this._events.ignore?.apply(this, arguments);
}
/**
* @name Two#type
* @property {String} - A string representing which type of renderer the instance has instantiated.
*/
type = "";
/**
* @name Two#renderer
* @property {(Two.SVGRenderer|Two.CanvasRenderer|Two.WebGLRenderer)} - The instantiated rendering class for the instance. For a list of possible rendering types check out Two.Types.
*/
renderer = null;
/**
* @name Two#scene
* @property {Two.Group} - The base level {@link Two.Group} which houses all objects for the instance. Because it is a {@link Two.Group} transformations can be applied to it that will affect all objects in the instance. This is handy as a makeshift inverted camera.
*/
scene = null;
/**
* @name Two#width
* @property {Number} - The width of the instance's dom element.
*/
width = 0;
/**
* @name Two#height
* @property {Number} - The height of the instance's dom element.
*/
height = 0;
/**
* @name Two#frameCount
* @property {Number} - An integer representing how many frames have elapsed.
*/
frameCount = 0;
/**
* @name Two#timeDelta
* @property {Number} - A number representing how much time has elapsed since the last frame in milliseconds.
*/
timeDelta = 0;
/**
* @name Two#playing
* @property {Boolean} - A boolean representing whether or not the instance is being updated through the automatic `requestAnimationFrame`.
*/
playing = false;
constructor(options) {
const params = _.defaults(options || {}, {
fullscreen: false,
fitted: false,
width: 640,
height: 480,
type: _Two.Types.svg,
autostart: false
});
_.each(
params,
function(v, k) {
if (/fullscreen/i.test(k) || /autostart/i.test(k)) {
return;
}
this[k] = v;
},
this
);
if (_.isElement(params.domElement)) {
const tagName = params.domElement.tagName.toLowerCase();
if (!/^(CanvasRenderer-canvas|WebGLRenderer-canvas|SVGRenderer-svg)$/.test(
this.type + "-" + tagName
)) {
this.type = _Two.Types[tagName];
}
}
this.renderer = new _Two[this.type](this);
this.setPlaying(params.autostart);
this.frameCount = 0;
if (params.fullscreen) {
this.fit = fitToWindow.bind(this);
this.fit.domElement = window;
this.fit.attached = true;
_.extend(document.body.style, {
overflow: "hidden",
margin: 0,
padding: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
position: "fixed"
});
_.extend(this.renderer.domElement.style, {
display: "block",
top: 0,
left: 0,
right: 0,
bottom: 0,
position: "fixed"
});
dom.bind(this.fit.domElement, "resize", this.fit);
this.fit();
} else if (params.fitted) {
this.fit = fitToParent.bind(this);
_.extend(this.renderer.domElement.style, {
display: "block"
});
} else if (typeof params.width === "number" && typeof params.height === "number") {
this.renderer.setSize(params.width, params.height, this.ratio);
this.width = params.width;
this.height = params.height;
}
this.renderer.bind(Events.Types.resize, updateDimensions.bind(this));
this.scene = this.renderer.scene;
_Two.Instances.push(this);
if (params.autostart) {
raf.init();
}
}
static NextFrameId = Constants.NextFrameId;
// Primitive
/**
* @name Two.Types
* @property {Object} - The different rendering types available in the library.
*/
static Types = Constants.Types;
/**
* @name Two.Version
* @property {String} - The current working version of the library, `$version`.
*/
static Version = Constants.Version;
/**
* @name Two.PublishDate
* @property {String} - The automatically generated publish date in the build process to verify version release candidates.
*/
static PublishDate = Constants.PublishDate;
/**
* @name Two.Identifier
* @property {String} - String prefix for all Two.js object's ids. This trickles down to SVG ids.
*/
static Identifier = Constants.Identifier;
/**
* @name Two.Resolution
* @property {Number} - Default amount of vertices to be used for interpreting Arcs and ArcSegments.
*/
static Resolution = Constants.Resolution;
/**
* @name Two.AutoCalculateImportedMatrices
* @property {Boolean} - When importing SVGs through the {@link Two#interpret} and {@link Two#load}, this boolean determines whether Two.js infers and then overrides the exact transformation matrix of the reference SVG.
* @nota-bene `false` copies the exact transformation matrix values, but also sets the path's `matrix.manual = true`.
*/
static AutoCalculateImportedMatrices = Constants.AutoCalculateImportedMatrices;
/**
* @name Two.Instances
* @property {Two[]} - Registered list of all Two.js instances in the current session.
*/
static Instances = Constants.Instances;
/**
* @function Two.uniqueId
* @description Simple method to access an incrementing value. Used for `id` allocation on all Two.js objects.
* @returns {Number} Ever increasing Number.
*/
static uniqueId = Constants.uniqueId;
static Anchor = Anchor;
static Collection = Collection;
static Events = Events;
static Group = Group;
static Matrix = Matrix2;
static Path = Path;
static Registry = Registry;
static Element = Element;
static Shape = Shape;
static Text = Text;
static Vector = Vector;
static Gradient = Gradient;
static Image = Image2;
static ImageSequence = ImageSequence;
static LinearGradient = LinearGradient;
static RadialGradient = RadialGradient;
static Sprite = Sprite;
static Stop = Stop;
static Texture = Texture;
static ArcSegment = ArcSegment;
static Circle = Circle;
static Ellipse = Ellipse;
static Line = Line;
static Points = Points;
static Polygon = Polygon;
static Rectangle = Rectangle;
static RoundedRectangle = RoundedRectangle;
static Star = Star;
static CanvasRenderer = Renderer;
static SVGRenderer = Renderer2;
static WebGLRenderer = Renderer3;
/**
* @name Two.Commands
* @property {Object} - Map of possible path commands. Taken from the SVG specification. Commands include: `move`, `line`, `curve`, `arc`, and `close`.
*/
static Commands = Commands;
/**
* @name Two.Utils
* @property {Object} Utils - A massive object filled with utility functions and properties.
* @property {Object} Two.Utils.read - A collection of SVG parsing functions indexed by element name.
* @property {Function} Two.Utils.read.path - Parse SVG path element or `d` attribute string.
*/
static Utils = Utils;
/**
* @name Two#appendTo
* @function
* @param {Element} elem - The DOM element to append the Two.js stage to.
* @description Shorthand method to append your instance of Two.js to the `document`.
*/
appendTo(elem) {
elem.appendChild(this.renderer.domElement);
if (this.fit) {
if (this.fit.domElement !== window) {
this.fit.domElement = elem;
this.fit.attached = false;
}
this.update();
}
return this;
}
/**
* @name Two#play
* @function
* @fires play
* @description Call to start an internal animation loop.
* @nota-bene This function initiates a `requestAnimationFrame` loop.
*/
play() {
this.playing = true;
raf.init();
return this.trigger(Events.Types.play);
}
/**
* @name Two#pause
* @function
* @fires pause
* @description Call to stop the internal animation loop for a specific instance of Two.js.
*/
pause() {
this.playing = false;
return this.trigger(Events.Types.pause);
}
setPlaying(p) {
this.playing = p;
}
/**
* @name Two#release
* @function
* @param {Two.Element} [obj] - Object to release from event listening. If none provided then the root {@link Two.Group} will be used.
* @returns {Two.Element} The object passed for event deallocation.
* @description Release a {@link Two.Element}’s events from memory and recurse through its children, effects, and/or vertices.
*/
release(obj) {
let i, v, child;
if (typeof obj === "undefined") {
return this.release(this.scene);
}
if (typeof obj.unbind === "function") {
obj.unbind();
}
if (typeof obj.fill === "object" && typeof obj.fill.unbind === "function") {
obj.fill.unbind();
}
if (typeof obj.stroke === "object" && typeof obj.stroke.unbind === "function") {
obj.stroke.unbind();
}
if (obj.vertices) {
if (typeof obj.vertices.unbind === "function") {
try {
obj.vertices.unbind();
} catch (e) {
}
}
for (i = 0; i < obj.vertices.length; i++) {
v = obj.vertices[i];
if (typeof v.unbind === "function") {
v.unbind();
}
if (v.controls) {
if (v.controls.left && typeof v.controls.left.unbind === "function") {
v.controls.left.unbind();
}
if (v.controls.right && typeof v.controls.right.unbind === "function") {
v.controls.right.unbind();
}
}
}
}
if (obj.children) {
for (i = 0; i < obj.children.length; i++) {
child = obj.children[i];
this.release(child);
}
if (typeof obj.children.unbind === "function") {
try {
obj.children.unbind();
} catch (e) {
}
}
}
if (obj._renderer) {
if (obj._renderer.elem && obj._renderer.elem.parentNode) {
obj._renderer.elem.parentNode.removeChild(obj._renderer.elem);
delete obj._renderer.elem;
}
if (this.type === "WebGLRenderer" && this.renderer.ctx) {
const gl = this.renderer.ctx;
if (obj._renderer.texture) {
gl.deleteTexture(obj._renderer.texture);
delete obj._renderer.texture;
}
if (obj._renderer.positionBuffer) {
gl.deleteBuffer(obj._renderer.positionBuffer);
delete obj._renderer.positionBuffer;
}
if (obj._renderer.effect) {
obj._renderer.effect = null;
}
}
if (this.type === "CanvasRenderer" && obj._renderer.context) {
delete obj._renderer.context;
}
}
return obj;
}
/**
* @name Two#getShapesAtPoint
* @function
* @param {Number} x - X coordinate in world space.
* @param {Number} y - Y coordinate in world space.
* @param {Object} [options] - Hit test configuration.
* @param {Boolean} [options.visibleOnly=true] - Limit results to visible shapes.
* @param {Boolean} [options.includeGroups=false] - Include groups in the hit results.
* @param {('all'|'deepest')} [options.mode='all'] - Whether to return all intersecting shapes or only the top-most.
* @param {Boolean} [options.deepest] - Alias for `mode: 'deepest'`.
* @param {Number} [options.precision] - Segmentation precision for curved geometry.
* @param {Number} [options.tolerance=0] - Pixel tolerance applied to hit testing.
* @param {Boolean} [options.fill] - Override fill testing behaviour.
* @param {Boolean} [options.stroke] - Override stroke testing behaviour.
* @param {Function} [options.filter] - Predicate to filter shapes from the result set.
* @returns {Two.Shape[]} Ordered list of shapes under the specified point, front to back.
* @description Returns shapes underneath the provided coordinates. Coordinates are expected in world space (matching the renderer output).
* @nota-bene Delegates to {@link Two.Group#getShapesAtPoint} on the root scene.
*/
getShapesAtPoint(x, y, options) {
if (this.scene && typeof this.scene.getShapesAtPoint === "function") {
return this.scene.getShapesAtPoint(x, y, options);
}
return [];
}
/**
* @name Two#update
* @function
* @fires update
* @description Update positions and calculations in one pass before rendering. Then render to the canvas.
* @nota-bene This function is called automatically if using {@link Two#play} or the `autostart` parameter in construction.
*/
update() {
const animated = !!this._lastFrame;
const now = _.performance.now();
if (animated) {
this.timeDelta = parseFloat((now - this._lastFrame).toFixed(3));
}
this._lastFrame = now;
if (this.fit && this.fit.domElement && !this.fit.attached) {
dom.bind(this.fit.domElement, "resize", this.fit);
this.fit.attached = true;
this.fit();
}
const width = this.width;
const height = this.height;
const renderer = this.renderer;
if (width !== renderer.width || height !== renderer.height) {
renderer.setSize(width, height, this.ratio);
}
this.trigger(Events.Types.update, this.frameCount, this.timeDelta);
return this.render();
}
/**
* @name Two#render
* @function
* @fires render
* @description Render all drawable and visible objects of the scene.
*/
render() {
this.renderer.render();
return this.trigger(Events.Types.render, this.frameCount++);
}
// Convenience Methods
/**
* @name Two#add
* @function
* @param {(Two.Shape[]|...Two.Shape)} [objects] - An array of Two.js objects. Alternatively can add objects as individual arguments.
* @description A shorthand method to add specific Two.js objects to the scene.
*/
add(objects) {
if (!(objects instanceof Array)) {
objects = Array.prototype.slice.call(arguments);
}
this.scene.add(objects);
return this;
}
/**
* @name Two#remove
* @function
* @param {(Two.Shape[]|...Two.Shape)} [objects] - An array of Two.js objects.
* @description A shorthand method to remove specific Two.js objects from the scene.
*/
remove(objects) {
if (!(objects instanceof Array)) {
objects = Array.prototype.slice.call(arguments);
}
this.scene.remove(objects);
return this;
}
/**
* @name Two#clear
* @function
* @description Removes all objects from the instance's scene. If you intend to have the browser garbage collect this, don't forget to delete the references in your application as well.
*/
clear() {
this.scene.remove(this.scene.children);
return this;
}
/**
* @name Two#makeLine
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* @returns {Two.Line}
* @description Creates a Two.js line and adds it to the scene.
*/
makeLine(x1, y1, x2, y2) {
const line = new Line(x1, y1, x2, y2);
this.scene.add(line);
return line;
}
/**
* @name Two#makeArrow
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* @returns {Two.Path}
* @description Creates a Two.js arrow and adds it to the scene.
*/
makeArrow(x1, y1, x2, y2, size) {
const headlen = typeof size === "number" ? size : 10;
const angle = Math.atan2(y2 - y1, x2 - x1);
const vertices = [
new Anchor(
x1,
y1,
void 0,
void 0,
void 0,
void 0,
Commands.move
),
new Anchor(
x2,
y2,
void 0,
void 0,
void 0,
void 0,
Commands.line
),
new Anchor(
x2 - headlen * Math.cos(angle - Math.PI / 4),
y2 - headlen * Math.sin(angle - Math.PI / 4),
void 0,
void 0,
void 0,
void 0,
Commands.line
),
new Anchor(
x2,
y2,
void 0,
void 0,
void 0,
void 0,
Commands.move
),
new Anchor(
x2 - headlen * Math.cos(angle + Math.PI / 4),
y2 - headlen * Math.sin(angle + Math.PI / 4),
void 0,
void 0,
void 0,
void 0,
Commands.line
)
];
const path = new Path(vertices, false, false, true);
path.noFill();
path.cap = "round";
path.join = "round";
this.scene.add(path);
return path;
}
/**
* @name Two#makeRectangle
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} width
* @param {Number} height
* @returns {Two.Rectangle}
* @description Creates a Two.js rectangle and adds it to the scene.
*/
makeRectangle(x, y, width, height) {
const rect = new Rectangle(x, y, width, height);
this.scene.add(rect);
return rect;
}
/**
* @name Two#makeRoundedRectangle
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} width
* @param {Number} height
* @param {Number} sides
* @returns {Two.RoundedRectangle}
* @description Creates a Two.js rounded rectangle and adds it to the scene.
*/
makeRoundedRectangle(x, y, width, height, sides) {
const rect = new RoundedRectangle(x, y, width, height, sides);
this.scene.add(rect);
return rect;
}
/**
* @name Two#makeCircle
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} radius
* @param {Number} [resolution=4]
* @returns {Two.Circle}
* @description Creates a Two.js circle and adds it to the scene.
*/
makeCircle(x, y, radius, resolution) {
const circle = new Circle(x, y, radius, resolution);
this.scene.add(circle);
return circle;
}
/**
* @name Two#makeEllipse
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} rx
* @param {Number} ry
* @param {Number} [resolution=4]
* @returns {Two.Ellipse}
* @description Creates a Two.js ellipse and adds it to the scene.
*/
makeEllipse(x, y, rx, ry, resolution) {
const ellipse = new Ellipse(x, y, rx, ry, resolution);
this.scene.add(ellipse);
return ellipse;
}
/**
* @name Two#makeStar
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} outerRadius
* @param {Number} innerRadius
* @param {Number} sides
* @returns {Two.Star}
* @description Creates a Two.js star and adds it to the scene.
*/
makeStar(x, y, outerRadius, innerRadius, sides) {
const star = new Star(x, y, outerRadius, innerRadius, sides);
this.scene.add(star);
return star;
}
/**
* @name Two#makeCurve
* @function
* @param {Two.Anchor[]} [points] - An array of {@link Two.Anchor} points.
* @param {...Number} - Alternatively you can pass alternating `x` / `y` coordinate values as individual arguments. These will be combined into {@link Two.Anchor}s for use in the path.
* @returns {Two.Path} - Where `path.curved` is set to `true`.
* @description Creates a Two.js path that is curved and adds it to the scene.
* @nota-bene In either case of passing an array or passing numbered arguments the last argument is an optional `Boolean` that defines whether the path should be open or closed.
*/
makeCurve(points) {
const l = arguments.length;
if (!Array.isArray(points)) {
points = [];
for (let i = 0; i < l; i += 2) {
const x = arguments[i];
if (typeof x !== "number") {
break;
}
const y = arguments[i + 1];
points.push(new Anchor(x, y));
}
}
const last = arguments[l - 1];
const curve = new Path(
points,
!(typeof last === "boolean" ? last : void 0),
true
);
const rect = curve.getBoundingClientRect();
curve.center().translation.set(rect.left + rect.width / 2, rect.top + rect.height / 2);
this.scene.add(curve);
return curve;
}
/**
* @name Two#makePolygon
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} radius
* @param {Number} sides
* @returns {Two.Polygon}
* @description Creates a Two.js polygon and adds it to the scene.
*/
makePolygon(x, y, radius, sides) {
const poly = new Polygon(x, y, radius, sides);
this.scene.add(poly);
return poly;
}
/**
* @name Two#makeArcSegment
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} innerRadius
* @param {Number} outerRadius
* @param {Number} startAngle
* @param {Number} endAngle
* @param {Number} [resolution=Two.Resolution] - The number of vertices that should comprise the arc segment.
* @returns {Two.ArcSegment}
*/
makeArcSegment(x, y, innerRadius, outerRadius, startAngle, endAngle, resolution) {
const arcSegment = new ArcSegment(
x,
y,
innerRadius,
outerRadius,
startAngle,
endAngle,
resolution
);
this.scene.add(arcSegment);
return arcSegment;
}
/**
* @name Two#makePoints
* @function
* @param {Two.Vector[]} [points] - An array of {@link Two.Vector} points
* @param {...Number} - Alternatively you can pass alternating `x` / `y` coordinate values as individual agrguments. These will be combined into {@link Two.Vector}s for use in the points object.
* @returns {Two.Points}
* @description Creates a Two.js points object and adds it to the current scene.
*/
makePoints(p) {
const l = arguments.length;
let vertices = p;
if (!Array.isArray(p)) {
vertices = [];
for (let i = 0; i < l; i += 2) {
const x = arguments[i];
if (typeof x !== "number") {
break;
}
const y = arguments[i + 1];
vertices.push(new Vector(x, y));
}
}
const points = new Points(vertices);
this.scene.add(points);
return points;
}
/**
* @name Two#makePath
* @function
* @param {Two.Anchor[]} [points] - An array of {@link Two.Anchor} points
* @param {...Number} - Alternatively you can pass alternating `x` / `y` coordinate values as individual arguments. These will be combined into {@link Two.Anchor}s for use in the path.
* @returns {Two.Path}
* @description Creates a Two.js path and adds it to the scene.
* @nota-bene In either case of passing an array or passing numbered arguments the last argument is an optional `Boolean` that defines whether the path should be open or closed.
*/
makePath(p) {
const l = arguments.length;
let points = p;
if (!Array.isArray(p)) {
points = [];
for (let i = 0; i < l; i += 2) {
const x = arguments[i];
if (typeof x !== "number") {
break;
}
const y = arguments[i + 1];
points.push(new Anchor(x, y));
}
}
const last = arguments[l - 1];
const path = new Path(
points,
!(typeof last === "boolean" ? last : void 0)
);
const rect = path.getBoundingClientRect();
if (typeof rect.top === "number" && typeof rect.left === "number" && typeof rect.right === "number" && typeof rect.bottom === "number") {
path.center().translation.set(
rect.left + rect.width / 2,
rect.top + rect.height / 2
);
}
this.scene.add(path);
return path;
}
/**
* @name Two#makeText
* @function
* @param {String} message
* @param {Number} x
* @param {Number} y
* @param {Object} [styles] - An object to describe any of the {@link Two.Text.Properties} including `fill`, `stroke`, `linewidth`, `family`, `alignment`, `leading`, `opacity`, etc..
* @returns {Two.Text}
* @description Creates a Two.js text object and adds it to the scene.
*/
makeText(message, x, y, styles) {
const text = new Text(message, x, y, styles);
this.add(text);
return text;
}
/**
* @name Two#makeLinearGradient
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* @param {...Two.Stop} args - Any number of color stops sometimes referred to as ramp stops. If none are supplied then the default black-to-white two stop gradient is applied.
* @returns {Two.LinearGradient}
* @description Creates a Two.js linear gradient and adds it to the scene. In the case of an effect it's added to an invisible "definitions" group.
*/
makeLinearGradient(x1, y1, x2, y2) {
const stops = Array.prototype.slice.call(arguments, 4);
const gradient = new LinearGradient(x1, y1, x2, y2, stops);
this.add(gradient);
return gradient;
}
/**
* @name Two#makeRadialGradient
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} radius
* @param {...Two.Stop} args - Any number of color stops sometimes referred to as ramp stops. If none are supplied then the default black-to-white two stop gradient is applied.
* @returns {Two.RadialGradient}
* @description Creates a Two.js linear-gradient object and adds it to the scene. In the case of an effect it's added to an invisible "definitions" group.
*/
makeRadialGradient(x1, y1, radius) {
const stops = Array.prototype.slice.call(arguments, 3);
const gradient = new RadialGradient(x1, y1, radius, stops);
this.add(gradient);
return gradient;
}
/**
* @name Two#makeSprite
* @function
* @param {(String|Two.Texture)} src - The URL path to an image or an already created {@link Two.Texture}.
* @param {Number} x
* @param {Number} y
* @param {Number} [columns=1]
* @param {Number} [rows=1]
* @param {Number} [frameRate=0]
* @param {Boolean} [autostart=false]
* @returns {Two.Sprite}
* @description Creates a Two.js sprite object and adds it to the scene. Sprites can be used for still images as well as animations.
*/
makeSprite(src, x, y, columns, rows, frameRate, autostart) {
const sprite = new Sprite(src, x, y, columns, rows, frameRate);
if (autostart) {
sprite.play();
}
this.add(sprite);
return sprite;
}
/**
* @name Two#makeImage
* @function
* @param {(String|Two.Texture)} src - The URL path to an image or an already created {@link Two.Texture}.
* @param {Number} x
* @param {Number} y
* @param {Number} width
* @param {Number} height
* @param {String} [mode="fill"]
* @returns {Two.Image}
* @description Creates a Two.js image object and adds it to the scene. Images are scaled to fit the provided width and height.
*/
makeImage(src, x, y, width, height, mode) {
const image = new Image2(src, x, y, width, height, mode);
this.add(image);
return image;
}
/**
* @name Two#makeImageSequence
* @function
* @param {(String[]|Two.Texture[])} src - An array of paths or of {@link Two.Textures}.
* @param {Number} x
* @param {Number} y
* @param {Number} [frameRate=0]
* @param {Boolean} [autostart=false]
* @returns {Two.ImageSequence}
* @description Creates a Two.js image sequence object and adds it to the scene.
*/
makeImageSequence(src, x, y, frameRate, autostart) {
const imageSequence = new ImageSequence(src, x, y, frameRate);
if (autostart) {
imageSequence.play();
}
this.add(imageSequence);
return imageSequence;
}
/**
* @name Two#makeTexture
* @function
* @param {(String|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement)} [src] - The URL path to an image or a DOM image-like element.
* @param {Function} [callback] - Function to be invoked when the image is loaded.
* @returns {Two.Texture}
* @description Creates a Two.js texture object.
*/
makeTexture(src, callback) {
const texture = new Texture(src, callback);
return texture;
}
/**
* @name Two#makeGroup
* @function
* @param {(Two.Shape[]|...Two.Shape)} [objects] - Two.js objects to be added to the group in the form of an array or as individual arguments.
* @returns {Two.Group}
* @description Creates a Two.js group object and adds it to the scene.
*/
makeGroup(objects) {
if (!(objects instanceof Array)) {
objects = Array.prototype.slice.call(arguments);
}
const group = new Group();
this.scene.add(group);
group.add(objects);
return group;
}
/**
* @name Two#interpret
* @function
* @param {SVGElement} svg - The SVG node to be parsed.
* @param {Boolean} shallow - Don't create a top-most group but append all content directly.
* @param {Boolean} [add=true] – Automatically add the reconstructed SVG node to scene.
* @returns {Two.Group}
* @description Interpret an SVG Node and add it to this instance's scene. The distinction should be made that this doesn't `import` svg's, it solely interprets them into something compatible for Two.js - this is slightly different than a direct transcription.
*/
interpret(svg2, shallow, add) {
const tag = svg2.tagName.toLowerCase();
add = typeof add !== "undefined" ? add : true;
if (!(tag in read)) {
return null;
}
const node = read[tag].call(this, svg2);
if (add) {
this.add(shallow && node instanceof Group ? node.children : node);
} else if (node.parent) {
node.remove();
}
return node;
}
/**
* @name Two#load
* @function
* @param {String|SVGElement} pathOrSVGContent - The URL path of an SVG file or an SVG document as text.
* @param {Function} [callback] - Function to call once loading has completed.
* @returns {Two.Group}
* @description Load an SVG file or SVG text and interpret it into Two.js legible objects.
*/
load(pathOrSVGContent, callback) {
const group = new Group();
let elem, i, child;
const attach = function(data) {
dom.temp.innerHTML = data;
for (i = 0; i < dom.temp.children.length; i++) {
elem = dom.temp.children[i];
child = this.interpret(elem, false, false);
if (child !== null) {
group.add(child);
}
}
if (typeof callback === "function") {
const svg2 = dom.temp.children.length <= 1 ? dom.temp.children[0] : dom.temp.children;
callback(group, svg2);
}
}.bind(this);
if (/\.svg$/i.test(pathOrSVGContent)) {
xhr(pathOrSVGContent, attach);
return group;
}
attach(pathOrSVGContent);
return group;
}
};
function fitToWindow() {
const wr = document.body.getBoundingClientRect();
const width = this.width = wr.width;
const height = this.height = wr.height;
this.renderer.setSize(width, height, this.ratio);
}
function fitToParent() {
const parent = this.renderer.domElement.parentElement;
if (!parent) {
console.warn("Two.js: Attempting to fit to parent, but no parent found.");
return;
}
const wr = parent.getBoundingClientRect();
const width = this.width = wr.width;
const height = this.height = wr.height;
this.renderer.setSize(width, height, this.ratio);
}
function updateDimensions(width, height) {
this.width = width;
this.height = height;
this.trigger(Events.Types.resize, width, height);
}
var raf = dom.getRequestAnimationFrame();
function loop() {
for (let i = 0; i < Two.Instances.length; i++) {
const t = Two.Instances[i];
if (t.playing) {
t.update();
}
}
Two.NextFrameId = raf(loop);
}
raf.init = function() {
loop();
raf.init = function() {
};
};
return __toCommonJS(two_exports);
})().default;
(function(){if(typeof exports==='object'&&typeof module!=='undefined'){module.exports=Two}})()
================================================
FILE: build/two.module.js
================================================
/*
MIT License
Copyright (c) 2012 - 2025 @jonobr1 / http://jono.fyi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __typeError = (msg) => {
throw TypeError(msg);
};
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
// src/utils/canvas-polyfill.js
var CanvasPolyfill = {
/**
* @param {Image}
*/
Image: null,
/**
* @param {Boolean}
*/
isHeadless: false,
/**
*
* @param {canvas} elem - An element to spoof as a ``.
* @param {String} [name] - An optional tag and node name to spoof. Defaults to `'canvas'`.
* @returns {canvas} - The same `elem` passed in the first argument with updated attributes needed to be used by Two.js.
* @description Adds attributes invoked by Two.js in order to execute and run correctly. This is used by headless environments.
*/
shim: function(elem, name) {
elem.tagName = elem.nodeName = name || "canvas";
elem.nodeType = 1;
elem.getAttribute = function(prop) {
return this[prop];
};
elem.setAttribute = function(prop, val) {
this[prop] = val;
return this;
};
return elem;
},
/**
* @name Two.Utils.polyfill
* @function
* @param {canvas} canvas - The instanced `Canvas` object provided by `node-canvas`.
* @param {Image} [Image] - The prototypical `Image` object provided by `node-canvas`. This is only necessary to pass if you're going to load bitmap imagery.
* @returns {canvas} Returns the instanced canvas object you passed from with additional attributes needed for Two.js.
* @description Convenience method for defining all the dependencies from the npm package `node-canvas`. See [node-canvas](https://github.com/Automattic/node-canvas) for additional information on setting up HTML5 `` drawing in a node.js environment.
*/
polyfill: function(canvas3, Image3) {
CanvasPolyfill.shim(canvas3);
if (typeof Image3 !== "undefined") {
CanvasPolyfill.Image = Image3;
}
CanvasPolyfill.isHeadless = true;
return canvas3;
}
};
// src/utils/curves.js
var curves_exports = {};
__export(curves_exports, {
Curve: () => Curve,
getAnchorsFromArcData: () => getAnchorsFromArcData,
getComponentOnCubicBezier: () => getComponentOnCubicBezier,
getControlPoints: () => getControlPoints,
getCurveBoundingBox: () => getCurveBoundingBox,
getCurveFromPoints: () => getCurveFromPoints,
getCurveLength: () => getCurveLength,
getReflection: () => getReflection,
integrate: () => integrate,
subdivide: () => subdivide
});
// src/utils/math.js
var math_exports = {};
__export(math_exports, {
HALF_PI: () => HALF_PI,
NumArray: () => NumArray,
TWO_PI: () => TWO_PI,
decomposeMatrix: () => decomposeMatrix,
getComputedMatrix: () => getComputedMatrix,
getEffectiveStrokeWidth: () => getEffectiveStrokeWidth,
getPoT: () => getPoT,
lerp: () => lerp,
mod: () => mod,
setMatrix: () => setMatrix,
toFixed: () => toFixed
});
// src/utils/root.js
var root;
if (typeof window !== "undefined") {
root = window;
} else if (typeof global !== "undefined") {
root = global;
} else if (typeof self !== "undefined") {
root = self;
}
// src/utils/math.js
var Matrix;
var TWO_PI = Math.PI * 2;
var HALF_PI = Math.PI * 0.5;
function decomposeMatrix(matrix, b, c, d, e, f) {
let a;
if (arguments.length <= 1) {
a = matrix.a;
b = matrix.b;
c = matrix.c;
d = matrix.d;
e = matrix.e;
f = matrix.f;
} else {
a = matrix;
}
return {
translateX: e,
translateY: f,
scaleX: Math.sqrt(a * a + b * b),
scaleY: Math.sqrt(c * c + d * d),
rotation: 180 * Math.atan2(b, a) / Math.PI
};
}
function setMatrix(matrix) {
Matrix = matrix;
}
function getComputedMatrix(object, matrix) {
matrix = matrix && matrix.identity() || new Matrix();
let parent = object;
const matrices = [];
while (parent && parent._matrix) {
matrices.push(parent._matrix);
parent = parent.parent;
}
matrices.reverse();
for (let i = 0; i < matrices.length; i++) {
const m = matrices[i];
const e = m.elements;
matrix.multiply(
e[0],
e[1],
e[2],
e[3],
e[4],
e[5],
e[6],
e[7],
e[8]
);
}
return matrix;
}
function lerp(a, b, t) {
return t * (b - a) + a;
}
var pots = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096];
function getPoT(value) {
let i = 0;
while (pots[i] && pots[i] < value) {
i++;
}
return pots[i];
}
function mod(v, l) {
while (v < 0) {
v += l;
}
return v % l;
}
var NumArray = root.Float32Array || Array;
var floor = Math.floor;
function toFixed(v) {
return floor(v * 1e6) / 1e6;
}
function getEffectiveStrokeWidth(object, worldMatrix) {
const linewidth = object._linewidth;
if (object.strokeAttenuation) {
return linewidth;
}
if (!worldMatrix) {
worldMatrix = object.worldMatrix || getComputedMatrix(object);
}
const decomposed = decomposeMatrix(
worldMatrix.elements[0],
worldMatrix.elements[3],
worldMatrix.elements[1],
worldMatrix.elements[4],
worldMatrix.elements[2],
worldMatrix.elements[5]
);
const scale = Math.max(Math.abs(decomposed.scaleX), Math.abs(decomposed.scaleY));
return scale > 0 ? linewidth / scale : linewidth;
}
// src/utils/path-commands.js
var Commands = {
move: "M",
line: "L",
curve: "C",
arc: "A",
close: "Z"
};
// src/events.js
var Events = class {
constructor() {
__publicField(this, "_events", {});
__publicField(this, "_bound", false);
}
/**
* @name Two.Events#addEventListener
* @function
* @param {String} [name] - The name of the event to bind a function to.
* @param {Function} [handler] - The function to be invoked when the event is dispatched.
* @description Call to add a listener to a specific event name.
*/
addEventListener(name, handler) {
const list = this._events[name] || (this._events[name] = []);
list.push(handler);
this._bound = true;
return this;
}
/**
* @name Two.Events#on
* @function
* @description Alias for {@link Two.Events#addEventListener}.
*/
on() {
return this.addEventListener.apply(this, arguments);
}
/**
* @name Two.Events#bind
* @function
* @description Alias for {@link Two.Events#addEventListener}.
*/
bind() {
return this.addEventListener.apply(this, arguments);
}
/**
* @name Two.Events#removeEventListener
* @function
* @param {String} [name] - The name of the event intended to be removed.
* @param {Function} [handler] - The handler intended to be removed.
* @description Call to remove listeners from a specific event. If only `name` is passed then all the handlers attached to that `name` will be removed. If no arguments are passed then all handlers for every event on the obejct are removed.
*/
removeEventListener(name, handler) {
if (!this._events) {
return this;
}
if (!name && !handler) {
this._events = {};
this._bound = false;
return this;
}
const names = name ? [name] : Object.keys(this._events);
for (let i = 0, l = names.length; i < l; i++) {
name = names[i];
let list = this._events[name];
if (list) {
let events = [];
if (handler) {
for (let j = 0, k = list.length; j < k; j++) {
let e = list[j];
e = e.handler ? e.handler : e;
if (handler !== e) {
events.push(e);
}
}
}
this._events[name] = events;
}
}
return this;
}
/**
* @name Two.Events#off
* @function
* @description Alias for {@link Two.Events#removeEventListener}.
*/
off() {
return this.removeEventListener.apply(this, arguments);
}
/**
* @name Two.Events#unbind
* @function
* @description Alias for {@link Two.Events#removeEventListener}.
*/
unbind() {
return this.removeEventListener.apply(this, arguments);
}
/**
* @name Two.Events#dispatchEvent
* @function
* @param {String} name - The name of the event to dispatch.
* @param args - Anything can be passed after the name and those will be passed on to handlers attached to the event in the order they are passed.
* @description Call to trigger a custom event. Any additional arguments passed after the name will be passed along to the attached handlers.
*/
dispatchEvent(name) {
if (!this._events) {
return this;
}
const args = Array.prototype.slice.call(arguments, 1);
const events = this._events[name];
if (events) {
for (let i = 0; i < events.length; i++) {
events[i].call(this, ...args);
}
}
return this;
}
trigger() {
return this.dispatchEvent.apply(this, arguments);
}
listen(obj, name, handler) {
const scope = this;
if (obj) {
e.obj = obj;
e.name = name;
e.handler = handler;
obj.on(name, e);
}
function e() {
handler.apply(scope, arguments);
}
return scope;
}
ignore(obj, name, handler) {
obj.off(name, handler);
return this;
}
};
/**
* @name Two.Events.Types
* @property {Object} - Object of different types of Two.js specific events.
*/
__publicField(Events, "Types", {
play: "play",
pause: "pause",
update: "update",
render: "render",
resize: "resize",
change: "change",
remove: "remove",
insert: "insert",
order: "order",
load: "load"
});
__publicField(Events, "Methods", [
"addEventListener",
"on",
"removeEventListener",
"off",
"unbind",
"dispatchEvent",
"trigger",
"listen",
"ignore"
]);
// src/vector.js
var proto = {
x: {
enumerable: true,
get: function() {
return this._x;
},
set: function(v) {
if (this._x !== v) {
this._x = v;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
},
y: {
enumerable: true,
get: function() {
return this._y;
},
set: function(v) {
if (this._y !== v) {
this._y = v;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
}
};
var _Vector = class _Vector extends Events {
constructor(x = 0, y = 0) {
super();
/**
* @name Two.Vector#_x
* @private
*/
__publicField(this, "_x", 0);
/**
* @name Two.Vector#_y
* @private
*/
__publicField(this, "_y", 0);
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this.x = x;
this.y = y;
}
/**
* @name Two.Vector.add
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Two.Vector}
* @description Add two vectors together.
*/
static add(v1, v2) {
return new _Vector(v1.x + v2.x, v1.y + v2.y);
}
/**
* @name Two.Vector.sub
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Two.Vector}
* @description Subtract two vectors: `v2` from `v1`.
*/
static sub(v1, v2) {
return new _Vector(v1.x - v2.x, v1.y - v2.y);
}
/**
* @name Two.Vector.subtract
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
static subtract(v1, v2) {
return _Vector.sub(v1, v2);
}
/**
* @name Two.Vector.ratioBetween
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Number} The ratio betwen two points `v1` and `v2`.
*/
static ratioBetween(v1, v2) {
return (v1.x * v2.x + v1.y * v2.y) / (v1.length() * v2.length());
}
/**
* @name Two.Vector.angleBetween
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Number} The angle between points `v1` and `v2`.
*/
static angleBetween(v1, v2) {
if (arguments.length >= 4) {
const dx2 = arguments[0] - arguments[2];
const dy2 = arguments[1] - arguments[3];
return Math.atan2(dy2, dx2);
}
const dx = v1.x - v2.x;
const dy = v1.y - v2.y;
return Math.atan2(dy, dx);
}
/**
* @name Two.Vector.distanceBetween
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Number} The distance between points `v1` and `v2`. Distance is always positive.
*/
static distanceBetween(v1, v2) {
return Math.sqrt(_Vector.distanceBetweenSquared(v1, v2));
}
/**
* @name Two.Vector.distanceBetweenSquared
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Number} The squared distance between points `v1` and `v2`.
*/
static distanceBetweenSquared(v1, v2) {
const dx = v1.x - v2.x;
const dy = v1.y - v2.y;
return dx * dx + dy * dy;
}
//
/**
* @name Two.Vector#set
* @function
* @param {number} x - Value of `x` component
* @param {number} y - Value of `y` component
*/
set(x, y) {
this.x = x;
this.y = y;
return this;
}
/**
* @name Two.Vector#copy
* @function
* @param {Two.Vector} v - The {@link Two.Vector} to copy
* @description Copy the `x` / `y` components of another object {@link Two.Vector}.
*/
copy(v) {
this.x = v.x;
this.y = v.y;
return this;
}
/**
* @name Two.Vector#clear
* @function
* @description Set the `x` / `y` component values of the vector to zero.
*/
clear() {
this.x = 0;
this.y = 0;
return this;
}
/**
* @name Two.Vector#clone
* @function
* @description Create a new vector and copy the existing values onto the newly created instance.
* @return {Two.Vector}
*/
clone() {
return new _Vector(this.x, this.y);
}
/**
* @name Two.Vector#add
* @function
* @param {Two.Vector} v - The {@link Two.Vector} to add
* @description Add an object with `x` / `y` component values to the instance.
* @overloaded
*/
/**
* @name Two.Vector#add
* @function
* @param {Number} n - Number to add
* @description Add the **same** number to both `x` / `y` component values of the instance.
* @overloaded
*/
/**
* @name Two.Vector#add
* @function
* @param {Number} x - Number to add to `x` component
* @param {Number} y - Number to add to `y` component
* @description Add `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
add(x, y) {
if (arguments.length <= 0) {
return this;
} else if (arguments.length <= 1) {
if (typeof x === "number") {
this.x += x;
this.y += x;
} else if (x && typeof x.x === "number" && typeof x.y === "number") {
this.x += x.x;
this.y += x.y;
}
} else {
this.x += x;
this.y += y;
}
return this;
}
/**
* @name Two.Vector#addSelf
* @function
* @description Alias for {@link Two.Vector.add}.
*/
addSelf(v) {
return this.add.apply(this, arguments);
}
/**
* @name Two.Vector#sub
* @function
* @param {Two.Vector} v - The amount as a {@link Two.Vector} to subtract
* @description Subtract an object with `x` / `y` component values to the instance.
* @overloaded
*/
/**
* @name Two.Vector#sub
* @function
* @param {Number} n - Number to subtract
* @description Subtract the **same** number to both `x` / `y` component values of the instance.
* @overloaded
*/
/**
* @name Two.Vector#sub
* @function
* @param {Number} x - Number to subtract from `x` component
* @param {Number} y - Number to subtract from `y` component
* @description Subtract `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
sub(x, y) {
if (arguments.length <= 0) {
return this;
} else if (arguments.length <= 1) {
if (typeof x === "number") {
this.x -= x;
this.y -= x;
} else if (x && typeof x.x === "number" && typeof x.y === "number") {
this.x -= x.x;
this.y -= x.y;
}
} else {
this.x -= x;
this.y -= y;
}
return this;
}
/**
* @name Two.Vector#subtract
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
subtract() {
return this.sub.apply(this, arguments);
}
/**
* @name Two.Vector#subSelf
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
subSelf(v) {
return this.sub.apply(this, arguments);
}
/**
* @name Two.Vector#subtractSelf
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
subtractSelf(v) {
return this.sub.apply(this, arguments);
}
/**
* @name Two.Vector#multiply
* @function
* @param {Two.Vector} v - The {@link Two.Vector} to multiply
* @description Multiply an object with `x` / `y` component values to the instance.
* @overloaded
*/
/**
* @name Two.Vector#multiply
* @function
* @param {Number} n - The number to multiply
* @description Multiply the **same** number to both x / y component values of the instance.
* @overloaded
*/
/**
* @name Two.Vector#multiply
* @function
* @param {Number} x - The number to multiply to `x` component
* @param {Number} y - The number to multiply to `y` component
* @description Multiply `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
multiply(x, y) {
if (arguments.length <= 0) {
return this;
} else if (arguments.length <= 1) {
if (typeof x === "number") {
this.x *= x;
this.y *= x;
} else if (x && typeof x.x === "number" && typeof x.y === "number") {
this.x *= x.x;
this.y *= x.y;
}
} else {
this.x *= x;
this.y *= y;
}
return this;
}
/**
* @name Two.Vector#multiplySelf
* @function
* @description Alias for {@link Two.Vector.multiply}.
*/
multiplySelf(v) {
return this.multiply.apply(this, arguments);
}
/**
* @name Two.Vector#multiplyScalar
* @function
* @param {Number} s - The scalar to multiply by.
* @description Mulitiply the vector by a single number. Shorthand to call {@link Two.Vector#multiply} directly.
*/
multiplyScalar(s) {
return this.multiply(s);
}
/**
* @name Two.Vector#divide
* @function
* @param {Two.Vector} v - The {@link Two.Vector} to divide
* @description Divide an object with `x` / `y` component values to the instance.
* @overloaded
*/
/**
* @name Two.Vector#divide
* @function
* @param {Number} n - The number to divide
* @description Divide the **same** number to both x / y component values of the instance.
* @overloaded
*/
/**
* @name Two.Vector#divide
* @function
* @param {Number} x - The number to divide on the `x` component
* @param {Number} y - The number to divide on the `y` component
* @description Divide `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
divide(x, y) {
if (arguments.length <= 0) {
return this;
} else if (arguments.length <= 1) {
if (typeof x === "number") {
this.x /= x;
this.y /= x;
} else if (x && typeof x.x === "number" && typeof x.y === "number") {
this.x /= x.x;
this.y /= x.y;
}
} else {
this.x /= x;
this.y /= y;
}
if (isNaN(this.x)) {
this.x = 0;
}
if (isNaN(this.y)) {
this.y = 0;
}
return this;
}
/**
* @name Two.Vector#divideSelf
* @function
* @description Alias for {@link Two.Vector.divide}.
*/
divideSelf(v) {
return this.divide.apply(this, arguments);
}
/**
* @name Two.Vector#divideScalar
* @function
* @param {Number} s - The scalar to divide by.
* @description Divide the vector by a single number. Shorthand to call {@link Two.Vector#divide} directly.
*/
divideScalar(s) {
return this.divide(s);
}
/**
* @name Two.Vector#negate
* @function
* @description Invert each component's sign value.
*/
negate() {
return this.multiply(-1);
}
/**
* @name Two.Vector#dot
* @function
* @returns {Number}
* @description Get the [dot product](https://en.wikipedia.org/wiki/Dot_product) of the vector.
*/
dot(v) {
return this.x * v.x + this.y * v.y;
}
/**
* @name Two.Vector#length
* @function
* @returns {Number}
* @description Get the length of a vector.
*/
length() {
return Math.sqrt(this.lengthSquared());
}
/**
* @name Two.Vector#lengthSquared
* @function
* @returns {Number}
* @description Get the length of the vector to the power of two. Widely used as less expensive than {@link Two.Vector#length} because it isn't square-rooting any numbers.
*/
lengthSquared() {
return this.x * this.x + this.y * this.y;
}
/**
* @name Two.Vector#normalize
* @function
* @description Normalize the vector from negative one to one.
*/
normalize() {
return this.divideScalar(this.length());
}
/**
* @name Two.Vector#distanceTo
* @function
* @returns {Number}
* @description Get the distance between two vectors.
*/
distanceTo(v) {
return Math.sqrt(this.distanceToSquared(v));
}
/**
* @name Two.Vector#distanceToSquared
* @function
* @returns {Number}
* @description Get the distance between two vectors to the power of two. Widely used as less expensive than {@link Two.Vector#distanceTo} because it isn't square-rooting any numbers.
*/
distanceToSquared(v) {
const dx = this.x - v.x;
const dy = this.y - v.y;
return dx * dx + dy * dy;
}
/**
* @name Two.Vector#setLength
* @function
* @param {Number} l - length to set vector to.
* @description Set the length of a vector.
*/
setLength(l) {
return this.normalize().multiplyScalar(l);
}
/**
* @name Two.Vector#equals
* @function
* @param {Two.Vector} v - The vector to compare against.
* @param {Number} [eps=0.0001] - An options epsilon for precision.
* @returns {Boolean}
* @description Qualify if one vector roughly equal another. With a margin of error defined by epsilon.
*/
equals(v, eps) {
eps = typeof eps === "undefined" ? 1e-4 : eps;
return this.distanceTo(v) < eps;
}
/**
* @name Two.Vector#lerp
* @function
* @param {Two.Vector} v - The destination vector to step towards.
* @param {Number} t - The zero to one value of how close the current vector gets to the destination vector.
* @description Linear interpolate one vector to another by an amount `t` defined as a zero to one number.
* @see [Matt DesLauriers](https://twitter.com/mattdesl/status/1031305279227478016) has a good thread about this.
*/
lerp(v, t) {
const x = (v.x - this.x) * t + this.x;
const y = (v.y - this.y) * t + this.y;
return this.set(x, y);
}
/**
* @name Two.Vector#isZero
* @function
* @param {Number} [eps=0.0001] - Optional precision amount to check against.
* @returns {Boolean}
* @description Check to see if vector is roughly zero, based on the `epsilon` precision value.
*/
isZero(eps) {
eps = typeof eps === "undefined" ? 1e-4 : eps;
return this.length() < eps;
}
/**
* @name Two.Vector#toString
* @function
* @returns {String}
* @description Return a comma-separated string of x, y value. Great for storing in a database.
*/
toString() {
return this.x + ", " + this.y;
}
/**
* @name Two.Vector#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the vector.
*/
toObject() {
return { x: toFixed(this.x), y: toFixed(this.y) };
}
/**
* @name Two.Vector#rotate
* @function
* @param {Number} radians - The amount to rotate the vector by in radians.
* @description Rotate a vector.
*/
rotate(radians) {
const x = this.x;
const y = this.y;
const cos7 = Math.cos(radians);
const sin7 = Math.sin(radians);
this.x = x * cos7 - y * sin7;
this.y = x * sin7 + y * cos7;
return this;
}
};
/**
* @name Two.Vector.zero
* @readonly
* @property {Two.Vector} - Handy reference to a vector with component values 0, 0 at all times.
*/
__publicField(_Vector, "zero", new _Vector());
/**
* @name Two.Vector.left
* @readonly
* @property {Two.Vector} - Handy reference to a vector with component values -1, 0 at all times.
*/
__publicField(_Vector, "left", new _Vector(-1, 0));
/**
* @name Two.Vector.right
* @readonly
* @property {Two.Vector} - Handy reference to a vector with component values 1, 0 at all times.
*/
__publicField(_Vector, "right", new _Vector(1, 0));
/**
* @name Two.Vector.up
* @readonly
* @property {Two.Vector} - Handy reference to a vector with component values 0, -1 at all times.
*/
__publicField(_Vector, "up", new _Vector(0, -1));
/**
* @name Two.Vector.down
* @readonly
* @property {Two.Vector} - Handy reference to a vector with component values 0, 1 at all times.
*/
__publicField(_Vector, "down", new _Vector(0, 1));
var Vector = _Vector;
// src/anchor.js
var Anchor = class _Anchor extends Vector {
constructor(x = 0, y = 0, ax = 0, ay = 0, bx = 0, by = 0, command = Commands.move) {
super(x, y);
__publicField(this, "controls", {
left: new Vector(),
right: new Vector()
});
__publicField(this, "_command", Commands.move);
__publicField(this, "_relative", true);
__publicField(this, "_rx", 0);
__publicField(this, "_ry", 0);
__publicField(this, "_xAxisRotation", 0);
__publicField(this, "_largeArcFlag", 0);
__publicField(this, "_sweepFlag", 1);
for (let prop in proto2) {
Object.defineProperty(this, prop, proto2[prop]);
}
this.command = command;
this.relative = true;
const broadcast = _Anchor.makeBroadcast(this);
this.controls.left.set(ax, ay).addEventListener(Events.Types.change, broadcast);
this.controls.right.set(bx, by).addEventListener(Events.Types.change, broadcast);
}
static makeBroadcast(scope) {
return broadcast;
function broadcast() {
if (scope._bound) {
scope.dispatchEvent(Events.Types.change);
}
}
}
/**
* @name Two.Anchor.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Anchor} to create a new instance
* @returns {Two.Anchor}
* @description Create a new {@link Two.Anchor} from an object notation of a {@link Two.Anchor}.
* @nota-bene Works in conjunction with {@link Two.Anchor#toObject}
*/
static fromObject(obj) {
return new _Anchor().copy(obj);
}
/**
* @name Two.Anchor#copy
* @function
* @param {Two.Anchor} v - The anchor to apply values to.
* @description Copy the properties of one {@link Two.Anchor} onto another.
*/
copy(v) {
this.x = v.x;
this.y = v.y;
if (typeof v.command === "string") {
this.command = v.command;
}
if (v.controls) {
if (v.controls.left) {
this.controls.left.copy(v.controls.left);
}
if (v.controls.right) {
this.controls.right.copy(v.controls.right);
}
}
if (typeof v.relative === "boolean") {
this.relative = v.relative;
}
if (typeof v.rx === "number") {
this.rx = v.rx;
}
if (typeof v.ry === "number") {
this.ry = v.ry;
}
if (typeof v.xAxisRotation === "number") {
this.xAxisRotation = v.xAxisRotation;
}
if (typeof v.largeArcFlag === "number") {
this.largeArcFlag = v.largeArcFlag;
}
if (typeof v.sweepFlag === "number") {
this.sweepFlag = v.sweepFlag;
}
return this;
}
/**
* @name Two.Anchor#clone
* @function
* @returns {Two.Anchor}
* @description Create a new {@link Two.Anchor}, set all its values to the current instance and return it for use.
*/
clone() {
return new _Anchor().copy(this);
}
/**
* @name Two.Anchor#toObject
* @function
* @returns {Object} - An object with properties filled out to mirror {@link Two.Anchor}.
* @description Create a JSON compatible plain object of the current instance. Intended for use with storing values in a database.
* @nota-bene Works in conjunction with {@link Two.Anchor.fromObject}
*/
toObject() {
return {
x: toFixed(this.x),
y: toFixed(this.y),
command: this.command,
relative: this.relative,
controls: {
left: this.controls.left.toObject(),
right: this.controls.right.toObject()
},
rx: toFixed(this.rx),
ry: toFixed(this.ry),
xAxisRotation: toFixed(this.xAxisRotation),
largeArcFlag: toFixed(this.largeArcFlag),
sweepFlag: toFixed(this.sweepFlag)
};
}
/**
* @name Two.Anchor#toString
* @function
* @returns {String} - A String with comma-separated values reflecting the various values on the current instance.
* @description Create a string form of the current instance. Intended for use with storing values in a database. This is lighter to store than the JSON compatible {@link Two.Anchor#toObject}.
*/
toString() {
return JSON.stringify(this.toObject());
}
};
var proto2 = {
command: {
enumerable: true,
get: function() {
return this._command;
},
set: function(command) {
if (this._command !== command) {
this._command = command;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
},
relative: {
enumerable: true,
get: function() {
return this._relative;
},
set: function(relative) {
if (this._relative !== !!relative) {
this._relative = !!relative;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
},
rx: {
enumerable: true,
get: function() {
return this._rx;
},
set: function(rx) {
if (this._rx !== rx) {
this._rx = rx;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
},
ry: {
enumerable: true,
get: function() {
return this._ry;
},
set: function(ry) {
if (this._ry !== ry) {
this._ry = ry;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
},
xAxisRotation: {
enumerable: true,
get: function() {
return this._xAxisRotation;
},
set: function(xAxisRotation) {
if (this._xAxisRotation !== xAxisRotation) {
this._xAxisRotation = xAxisRotation;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
},
largeArcFlag: {
enumerable: true,
get: function() {
return this._largeArcFlag;
},
set: function(largeArcFlag) {
if (this._largeArcFlag !== largeArcFlag) {
this._largeArcFlag = largeArcFlag;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
},
sweepFlag: {
get: function() {
return this._sweepFlag;
},
set: function(sweepFlag) {
if (this._sweepFlag !== sweepFlag) {
this._sweepFlag = sweepFlag;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
}
}
};
// src/constants.js
var count = 0;
var Constants = {
/**
* @name Two.NextFrameId
* @property {Number}
* @description The id of the next `requestAnimationFrame` function. Used to control the (or cancel) the default behavior of Two.js animation loops.
*/
NextFrameId: null,
// Primitive
/**
* @name Two.Types
* @property {Object} - The different rendering types available in the library.
*/
Types: {
webgl: "WebGLRenderer",
svg: "SVGRenderer",
canvas: "CanvasRenderer"
},
/**
* @name Two.Version
* @property {String} - The current working version of the library.
*/
Version: "v0.8.23",
/**
* @name Two.PublishDate
* @property {String} - The automatically generated publish date in the build process to verify version release candidates.
*/
PublishDate: "2026-01-05T18:28:31.207Z",
/**
* @name Two.Identifier
* @property {String} - String prefix for all Two.js object's ids. This trickles down to SVG ids.
*/
Identifier: "two-",
/**
* @name Two.Resolution
* @property {Number} - Default amount of vertices to be used for interpreting Arcs and ArcSegments.
*/
Resolution: 12,
/**
* @name Two.AutoCalculateImportedMatrices
* @property {Boolean} - When importing SVGs through the {@link Two#interpret} and {@link Two#load}, this boolean determines whether Two.js infers and then overrides the exact transformation matrix of the reference SVG.
* @nota-bene `false` copies the exact transformation matrix values, but also sets the path's `matrix.manual = true`.
*/
AutoCalculateImportedMatrices: true,
/**
* @name Two.Instances
* @property {Two[]} - Registered list of all Two.js instances in the current session.
*/
Instances: [],
/**
* @function Two.uniqueId
* @description Simple method to access an incrementing value. Used for `id` allocation on all Two.js objects.
* @returns {Number} Ever increasing Number.
*/
uniqueId: function() {
return count++;
}
};
// src/utils/curves.js
var Curve = {
CollinearityEpsilon: Math.pow(10, -30),
RecursionLimit: 16,
CuspLimit: 0,
Tolerance: {
distance: 0.25,
angle: 0,
epsilon: Number.EPSILON
},
// Lookup tables for abscissas and weights with values for n = 2 .. 16.
// As values are symmetric, only store half of them and adapt algorithm
// to factor in symmetry.
abscissas: [
[0.5773502691896],
[0, 0.7745966692415],
[0.3399810435849, 0.8611363115941],
[0, 0.5384693101057, 0.9061798459387],
[0.2386191860832, 0.6612093864663, 0.9324695142032],
[0, 0.4058451513774, 0.7415311855994, 0.9491079123428],
[0.1834346424956, 0.5255324099163, 0.7966664774136, 0.9602898564975],
[0, 0.3242534234038, 0.6133714327006, 0.8360311073266, 0.9681602395076],
[
0.1488743389816,
0.4333953941292,
0.679409568299,
0.865063366689,
0.9739065285172
],
[
0,
0.2695431559523,
0.5190961292068,
0.730152005574,
0.8870625997681,
0.9782286581461
],
[
0.1252334085115,
0.3678314989982,
0.5873179542866,
0.7699026741943,
0.9041172563705,
0.9815606342467
],
[
0,
0.2304583159551,
0.4484927510364,
0.6423493394403,
0.8015780907333,
0.917598399223,
0.9841830547186
],
[
0.1080549487073,
0.3191123689279,
0.5152486363582,
0.6872929048117,
0.8272013150698,
0.9284348836636,
0.9862838086968
],
[
0,
0.2011940939974,
0.3941513470776,
0.5709721726085,
0.7244177313602,
0.8482065834104,
0.9372733924007,
0.9879925180205
],
[
0.0950125098376,
0.2816035507793,
0.4580167776572,
0.6178762444026,
0.755404408355,
0.8656312023878,
0.9445750230732,
0.9894009349916
]
],
weights: [
[1],
[0.8888888888889, 0.5555555555556],
[0.6521451548625, 0.3478548451375],
[0.5688888888889, 0.4786286704994, 0.2369268850562],
[0.4679139345727, 0.3607615730481, 0.1713244923792],
[0.4179591836735, 0.3818300505051, 0.2797053914893, 0.1294849661689],
[0.3626837833784, 0.3137066458779, 0.2223810344534, 0.1012285362904],
[
0.3302393550013,
0.31234707704,
0.2606106964029,
0.1806481606949,
0.0812743883616
],
[
0.2955242247148,
0.26926671931,
0.219086362516,
0.1494513491506,
0.0666713443087
],
[
0.2729250867779,
0.2628045445102,
0.233193764592,
0.1862902109277,
0.1255803694649,
0.0556685671162
],
[
0.2491470458134,
0.2334925365384,
0.2031674267231,
0.1600783285433,
0.1069393259953,
0.0471753363865
],
[
0.2325515532309,
0.2262831802629,
0.2078160475369,
0.1781459807619,
0.1388735102198,
0.0921214998377,
0.0404840047653
],
[
0.2152638534632,
0.2051984637213,
0.1855383974779,
0.1572031671582,
0.1215185706879,
0.0801580871598,
0.0351194603318
],
[
0.2025782419256,
0.1984314853271,
0.1861610000156,
0.166269205817,
0.1395706779262,
0.1071592204672,
0.0703660474881,
0.0307532419961
],
[
0.1894506104551,
0.1826034150449,
0.169156519395,
0.1495959888166,
0.1246289712555,
0.0951585116825,
0.0622535239386,
0.0271524594118
]
]
};
function getComponentOnCubicBezier(t, a, b, c, d) {
const k = 1 - t;
return k * k * k * a + 3 * k * k * t * b + 3 * k * t * t * c + t * t * t * d;
}
function subdivide(x1, y1, x2, y2, x3, y3, x4, y4, limit) {
limit = limit || Curve.RecursionLimit;
const amount = limit + 1;
if (Math.abs(x1 - x4) < 1e-3 && Math.abs(y1 - y4) < 1e-3) {
return [new Anchor(x4, y4)];
}
const result = [];
for (let i = 0; i < amount; i++) {
const t = i / amount;
const x = getComponentOnCubicBezier(t, x1, x2, x3, x4);
const y = getComponentOnCubicBezier(t, y1, y2, y3, y4);
result.push(new Anchor(x, y));
}
return result;
}
function getCurveLength(x1, y1, x2, y2, x3, y3, x4, y4, limit) {
if (x1 === x2 && y1 === y2 && x3 === x4 && y3 === y4) {
const dx = x4 - x1;
const dy = y4 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
const ax = 9 * (x2 - x3) + 3 * (x4 - x1), bx = 6 * (x1 + x3) - 12 * x2, cx = 3 * (x2 - x1), ay = 9 * (y2 - y3) + 3 * (y4 - y1), by = 6 * (y1 + y3) - 12 * y2, cy = 3 * (y2 - y1);
function integrand(t) {
const dx = (ax * t + bx) * t + cx, dy = (ay * t + by) * t + cy;
return Math.sqrt(dx * dx + dy * dy);
}
return integrate(integrand, 0, 1, limit || Curve.RecursionLimit);
}
function getCurveBoundingBox(x1, y1, x2, y2, x3, y3, x4, y4) {
const tvalues = [];
const bounds = [[], []];
let a, b, c, t, t1, t2, b2ac, sqrtb2ac;
for (let i = 0; i < 2; ++i) {
if (i === 0) {
b = 6 * x1 - 12 * x2 + 6 * x3;
a = -3 * x1 + 9 * x2 - 9 * x3 + 3 * x4;
c = 3 * x2 - 3 * x1;
} else {
b = 6 * y1 - 12 * y2 + 6 * y3;
a = -3 * y1 + 9 * y2 - 9 * y3 + 3 * y4;
c = 3 * y2 - 3 * y1;
}
if (Math.abs(a) < 1e-3) {
if (Math.abs(b) < 1e-3) {
continue;
}
t = -c / b;
if (0 < t && t < 1) {
tvalues.push(t);
}
continue;
}
b2ac = b * b - 4 * c * a;
sqrtb2ac = Math.sqrt(b2ac);
if (b2ac < 0) {
continue;
}
t1 = (-b + sqrtb2ac) / (2 * a);
if (0 < t1 && t1 < 1) {
tvalues.push(t1);
}
t2 = (-b - sqrtb2ac) / (2 * a);
if (0 < t2 && t2 < 1) {
tvalues.push(t2);
}
}
let j = tvalues.length;
let jlen = j;
let mt;
while (j--) {
t = tvalues[j];
mt = 1 - t;
bounds[0][j] = mt * mt * mt * x1 + 3 * mt * mt * t * x2 + 3 * mt * t * t * x3 + t * t * t * x4;
bounds[1][j] = mt * mt * mt * y1 + 3 * mt * mt * t * y2 + 3 * mt * t * t * y3 + t * t * t * y4;
}
bounds[0][jlen] = x1;
bounds[1][jlen] = y1;
bounds[0][jlen + 1] = x4;
bounds[1][jlen + 1] = y4;
bounds[0].length = bounds[1].length = jlen + 2;
return {
min: { x: Math.min.apply(0, bounds[0]), y: Math.min.apply(0, bounds[1]) },
max: { x: Math.max.apply(0, bounds[0]), y: Math.max.apply(0, bounds[1]) }
};
}
function integrate(f, a, b, n) {
let x = Curve.abscissas[n - 2], w = Curve.weights[n - 2], A = 0.5 * (b - a), B = A + a, i = 0, m = n + 1 >> 1, sum = n & 1 ? w[i++] * f(B) : 0;
while (i < m) {
const Ax = A * x[i];
sum += w[i++] * (f(B + Ax) + f(B - Ax));
}
return A * sum;
}
function getCurveFromPoints(points, closed2) {
const l = points.length, last = l - 1;
for (let i = 0; i < l; i++) {
const point = points[i];
const prev = closed2 ? mod(i - 1, l) : Math.max(i - 1, 0);
const next = closed2 ? mod(i + 1, l) : Math.min(i + 1, last);
const a = points[prev];
const b = point;
const c = points[next];
getControlPoints(a, b, c);
b.command = i === 0 ? Commands.move : Commands.curve;
}
}
function getControlPoints(a, b, c) {
const a1 = Vector.angleBetween(a, b);
const a2 = Vector.angleBetween(c, b);
let d1 = Vector.distanceBetween(a, b);
let d2 = Vector.distanceBetween(c, b);
let mid = (a1 + a2) / 2;
if (d1 < 1e-3 || d2 < 1e-3) {
if (typeof b.relative === "boolean" && !b.relative) {
b.controls.left.copy(b);
b.controls.right.copy(b);
}
return b;
}
d1 *= 0.33;
d2 *= 0.33;
if (a2 < a1) {
mid += HALF_PI;
} else {
mid -= HALF_PI;
}
b.controls.left.x = Math.cos(mid) * d1;
b.controls.left.y = Math.sin(mid) * d1;
mid -= Math.PI;
b.controls.right.x = Math.cos(mid) * d2;
b.controls.right.y = Math.sin(mid) * d2;
if (typeof b.relative === "boolean" && !b.relative) {
b.controls.left.x += b.x;
b.controls.left.y += b.y;
b.controls.right.x += b.x;
b.controls.right.y += b.y;
}
return b;
}
function getReflection(a, b, relative) {
return new Vector(
2 * a.x - (b.x + a.x) - (relative ? a.x : 0),
2 * a.y - (b.y + a.y) - (relative ? a.y : 0)
);
}
function getAnchorsFromArcData(center, xAxisRotation, rx, ry, ts, td, ccw) {
const resolution = Constants.Resolution;
const anchors = [];
for (let i = 0; i < resolution; i++) {
let pct = (i + 1) / resolution;
if (ccw) {
pct = 1 - pct;
}
const theta = pct * td + ts;
const x = rx * Math.cos(theta);
const y = ry * Math.sin(theta);
const anchor2 = new Anchor(x, y);
anchor2.command = Commands.line;
anchors.push(anchor2);
}
}
// src/utils/underscore.js
var slice = Array.prototype.slice;
function isArrayLike(collection) {
if (collection === null || collection === void 0) return false;
const length = collection.length;
return typeof length == "number" && length >= 0 && length < 4294967296;
}
var _ = {
isNaN: function(obj) {
return typeof obj === "number" && obj !== +obj;
},
isElement: function(obj) {
return !!(obj && obj.nodeType === 1);
},
isObject: function(obj) {
const type = typeof obj;
return type === "function" || type === "object" && !!obj;
},
isFunction: function(obj) {
return typeof obj === "function";
},
extend: function(base) {
const sources = slice.call(arguments, 1);
for (let i = 0; i < sources.length; i++) {
const obj = sources[i];
for (let k in obj) {
base[k] = obj[k];
}
}
return base;
},
defaults: function(base) {
const sources = slice.call(arguments, 1);
for (let i = 0; i < sources.length; i++) {
const obj = sources[i];
for (let k in obj) {
if (base[k] === void 0) {
base[k] = obj[k];
}
}
}
return base;
},
each: function(obj, iteratee, context) {
const ctx = context || this;
const keys = !isArrayLike(obj) && Object.keys(obj);
const length = (keys || obj).length;
for (let i = 0; i < length; i++) {
const k = keys ? keys[i] : i;
iteratee.call(ctx, obj[k], k, obj);
}
return obj;
},
/**
* @name Two.Utils.performance
* @property {Date} - A special `Date` like object to get the current millis of the session. Used internally to calculate time between frames.
* e.g: `Utils.performance.now() // milliseconds since epoch`
*/
performance: root.performance && root.performance.now ? root.performance : Date
};
// src/utils/dom.js
var dom = {
hasEventListeners: typeof root.addEventListener === "function",
bind: function(elem, event, func, bool) {
if (this.hasEventListeners) {
elem.addEventListener(event, func, !!bool);
} else {
elem.attachEvent("on" + event, func);
}
return dom;
},
unbind: function(elem, event, func, bool) {
if (dom.hasEventListeners) {
elem.removeEventListeners(event, func, !!bool);
} else {
elem.detachEvent("on" + event, func);
}
return dom;
},
getRequestAnimationFrame: function() {
const vendors = ["ms", "moz", "webkit", "o"];
let lastTime = 0;
let request = root.requestAnimationFrame;
if (!request) {
for (let i = 0; i < vendors.length; i++) {
request = root[vendors[i] + "RequestAnimationFrame"] || request;
}
request = request || fallbackRequest;
}
function fallbackRequest(callback, element) {
const currTime = (/* @__PURE__ */ new Date()).getTime();
const timeToCall = Math.max(0, 16 - (currTime - lastTime));
const id = root.setTimeout(nextRequest, timeToCall);
lastTime = currTime + timeToCall;
function nextRequest() {
callback(currTime + timeToCall);
}
return id;
}
return request;
}
};
var temp = root.document ? root.document.createElement("div") : {};
temp.id = "help-two-load";
Object.defineProperty(dom, "temp", {
enumerable: true,
get: function() {
if (_.isElement(temp) && !root.document.head.contains(temp)) {
temp.style.display = "none";
root.document.head.appendChild(temp);
}
return temp;
}
});
// src/utils/error.js
var TwoError = class extends Error {
constructor(message) {
super();
__publicField(this, "name", "Two.js");
__publicField(this, "message");
this.message = message;
}
};
// src/utils/device-pixel-ratio.js
var devicePixelRatio = root.devicePixelRatio || 1;
function getBackingStoreRatio(ctx) {
return ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1;
}
function getRatio(ctx) {
return devicePixelRatio / getBackingStoreRatio(ctx);
}
// src/registry.js
var Registry = class {
constructor() {
__publicField(this, "map", {});
}
/**
* @name Two.Registry#add
* @function
* @param {String} id - A unique identifier.
* @param obj - Any type of variable to be registered to the directory.
* @description Adds any value to the directory. Assigned by the `id`.
*/
add(id, obj) {
this.map[id] = obj;
return this;
}
/**
* @name Two.Registry#remove
* @function
* @param {String} id - A unique identifier.
* @description Remove any value from the directory by its `id`.
*/
remove(id) {
delete this.map[id];
return this;
}
/**
* @name Two.Registry#get
* @function
* @param {String} id - A unique identifier.
* @returns {?Object} The associated value. If unavailable then `undefined` is returned.
* @description Get a registered value by its `id`.
*/
get(id) {
return this.map[id];
}
/**
* @name Two.Registry#contains
* @function
* @param {String} id - A unique identifier.
* @returns {Boolean}
* @description Convenience method to see if a value is registered to an `id` already.
*/
contains(id) {
return id in this.map;
}
};
// src/collection.js
var _events;
var Collection = class extends Array {
constructor() {
super();
// Warning: Multiple inheritance hack
/**
* @private
*/
__privateAdd(this, _events, new Events());
if (arguments[0] && Array.isArray(arguments[0])) {
if (arguments[0].length > 0) {
this.push.apply(this, arguments[0]);
}
} else if (arguments.length > 0) {
this.push.apply(this, arguments);
}
}
// N.B: Technique to disable enumeration on object
get _events() {
return __privateGet(this, _events);
}
set _events(e) {
__privateSet(this, _events, e);
}
// Getters and setters aren't enumerable
get _bound() {
return __privateGet(this, _events)._bound;
}
set _bound(v) {
__privateGet(this, _events)._bound = v;
}
addEventListener() {
var _a;
return (_a = __privateGet(this, _events).addEventListener) == null ? void 0 : _a.apply(this, arguments);
}
on() {
var _a;
return (_a = __privateGet(this, _events).on) == null ? void 0 : _a.apply(this, arguments);
}
bind() {
var _a;
return (_a = __privateGet(this, _events).bind) == null ? void 0 : _a.apply(this, arguments);
}
removeEventListener() {
var _a;
return (_a = __privateGet(this, _events).removeEventListener) == null ? void 0 : _a.apply(this, arguments);
}
off() {
var _a;
return (_a = __privateGet(this, _events).off) == null ? void 0 : _a.apply(this, arguments);
}
unbind() {
var _a;
return (_a = __privateGet(this, _events).unbind) == null ? void 0 : _a.apply(this, arguments);
}
dispatchEvent() {
var _a;
return (_a = __privateGet(this, _events).dispatchEvent) == null ? void 0 : _a.apply(this, arguments);
}
trigger() {
var _a;
return (_a = __privateGet(this, _events).trigger) == null ? void 0 : _a.apply(this, arguments);
}
listen() {
var _a;
return (_a = __privateGet(this, _events).listen) == null ? void 0 : _a.apply(this, arguments);
}
ignore() {
var _a;
return (_a = __privateGet(this, _events).ignore) == null ? void 0 : _a.apply(this, arguments);
}
pop() {
const popped = super.pop.apply(this, arguments);
this.trigger(Events.Types.remove, [popped]);
return popped;
}
shift() {
const shifted = super.shift.apply(this, arguments);
this.trigger(Events.Types.remove, [shifted]);
return shifted;
}
push() {
const pushed = super.push.apply(this, arguments);
this.trigger(Events.Types.insert, arguments);
return pushed;
}
unshift() {
const unshifted = super.unshift.apply(this, arguments);
this.trigger(Events.Types.insert, arguments);
return unshifted;
}
splice() {
const spliced = super.splice.apply(this, arguments);
this.trigger(Events.Types.remove, spliced);
if (arguments.length > 2) {
const inserted = this.slice(
arguments[0],
arguments[0] + arguments.length - 2
);
this.trigger(Events.Types.insert, inserted);
this.trigger(Events.Types.order);
}
return spliced;
}
sort() {
super.sort.apply(this, arguments);
this.trigger(Events.Types.order);
return this;
}
reverse() {
super.reverse.apply(this, arguments);
this.trigger(Events.Types.order);
return this;
}
indexOf() {
return super.indexOf.apply(this, arguments);
}
map(func, scope) {
const results = [];
for (let key = 0; key < this.length; key++) {
const value = this[key];
let result;
if (scope) {
result = func.call(scope, value, key);
} else {
result = func(value, key);
}
results.push(result);
}
return results;
}
};
_events = new WeakMap();
// src/element.js
var _Element = class _Element extends Events {
constructor() {
super();
/**
* @name Two.Element#_flagId
* @private
* @property {Boolean} - Determines whether the {@link Two.Element#id} needs updating.
*/
__publicField(this, "_flagId", false);
/**
* @name Two.Element#_flagClassName
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#className} need updating.
*/
__publicField(this, "_flagClassName", false);
/**
* @name Two.Element#renderer
* @property {Object} - Object access to store relevant renderer specific variables. Warning: manipulating this object can create unintended consequences.
* @nota-bene With the {@link Two.SVGRenderer} you can access the underlying SVG element created via `shape.renderer.elem`.
*/
__publicField(this, "_renderer", {});
/**
* @name Two.Element#id
* @property {String} - Session specific unique identifier.
* @nota-bene In the {@link Two.SVGRenderer} change this to change the underlying SVG element's id too.
*/
__publicField(this, "_id", Constants.Identifier + Constants.uniqueId());
/**
* @name Two.Element#className
* @property {String} - A class to be applied to the element to be compatible with CSS styling.
* @nota-bene Only rendered to DOM in the SVG renderer.
*/
__publicField(this, "_className", "");
/**
* @name Two.Element#classList
* @property {String[]}
* @description A list of class strings stored if imported / interpreted from an SVG element.
*/
__publicField(this, "classList", []);
for (let prop in proto3) {
Object.defineProperty(this, prop, proto3[prop]);
}
}
/**
* @name Two.Element.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Element} to create a new instance
* @returns {Two.Element}
* @description Create a new {@link Two.Element} from an object notation of a {@link Two.Element}.
* @nota-bene Works in conjunction with {@link Two.Element#toObject}
*/
static fromObject(obj) {
const elem = new _Element().copy(obj);
if ("id" in obj) {
elem.id = obj.id;
}
return elem;
}
/**
* @name Two.Element#flagReset
* @function
* @description Called internally by Two.js's renderer to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagId = this._flagClassName = false;
return this;
}
copy(element) {
if (element.renderer && typeof element.renderer.type === "string") {
this.renderer.type = element.renderer.type;
}
if (typeof element.className === "string") {
this.className = element.className;
}
return this;
}
toObject() {
return {
renderer: { type: this.renderer.type },
id: this.id,
className: this.className
};
}
/**
* @name Two.Element#dispose
* @function
* @description Release the element's renderer object and detach any events.
* This cleans up renderer-specific resources and unbinds all event listeners.
*/
dispose() {
if (typeof this.unbind === "function") {
this.unbind();
}
if (this._renderer) {
if (this._renderer.elem && this._renderer.elem.parentNode) {
this._renderer.elem.parentNode.removeChild(this._renderer.elem);
delete this._renderer.elem;
}
if (this.type === "WebGLRenderer" && this.renderer.ctx) {
const gl = this.renderer.ctx;
if (this._renderer.texture) {
gl.deleteTexture(this._renderer.texture);
delete this._renderer.texture;
}
if (this._renderer.positionBuffer) {
gl.deleteBuffer(this._renderer.positionBuffer);
delete this._renderer.positionBuffer;
}
if (this._renderer.effect) {
this._renderer.effect = null;
}
}
if (this.type === "CanvasRenderer" && this._renderer.context) {
delete this._renderer.context;
}
}
const rendererType = this._renderer.type;
this._renderer = { type: rendererType };
return this;
}
};
__publicField(_Element, "Properties", ["renderer", "id", "className"]);
var Element = _Element;
var proto3 = {
renderer: {
enumerable: false,
get: function() {
return this._renderer;
}
},
id: {
enumerable: true,
get: function() {
return this._id;
},
set: function(v) {
const id = this._id;
if (v === this._id) {
return;
}
this._id = v;
this._flagId = true;
if (this.parent) {
delete this.parent.children.ids[id];
this.parent.children.ids[this._id] = this;
}
}
},
className: {
enumerable: true,
get: function() {
return this._className;
},
set: function(v) {
if (this._className !== v) {
this._flagClassName = true;
this.classList = v.split(/\s+?/);
this._className = v;
}
}
}
};
// src/effects/texture.js
var anchor;
var regex = {
video: /\.(mp4|webm|ogg)$/i,
image: /\.(jpe?g|png|gif|tiff|webp)$/i,
effect: /texture|gradient/i
};
if (root.document) {
anchor = document.createElement("a");
}
var _Texture = class _Texture extends Element {
constructor(src, callback) {
super();
/**
* @name Two.Texture#_flagSrc
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#src} needs updating.
*/
__publicField(this, "_flagSrc", false);
/**
* @name Two.Texture#_flagImage
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#image} needs updating.
*/
__publicField(this, "_flagImage", false);
/**
* @name Two.Texture#_flagVideo
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#video} needs updating.
*/
__publicField(this, "_flagVideo", false);
/**
* @name Two.Texture#_flagLoaded
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#loaded} needs updating.
*/
__publicField(this, "_flagLoaded", false);
/**
* @name Two.Texture#_flagRepeat
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#repeat} needs updating.
*/
__publicField(this, "_flagRepeat", false);
/**
* @name Two.Texture#_flagOffset
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#offset} needs updating.
*/
__publicField(this, "_flagOffset", false);
/**
* @name Two.Texture#_flagScale
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#scale} needs updating.
*/
__publicField(this, "_flagScale", false);
/**
* @name Two.Texture#_src
* @private
* @see {@link Two.Texture#src}
*/
__publicField(this, "_src", "");
/**
* @name Two.Texture#_image
* @private
* @see {@link Two.Texture#image}
*/
__publicField(this, "_image", null);
/**
* @name Two.Texture#_loaded
* @private
* @see {@link Two.Texture#loaded}
*/
__publicField(this, "_loaded", false);
/**
* @name Two.Texture#_repeat
* @private
* @see {@link Two.Texture#repeat}
*/
__publicField(this, "_repeat", "no-repeat");
/**
* @name Two.Texture#_scale
* @private
* @see {@link Two.Texture#scale}
*/
__publicField(this, "_scale", 1);
/**
* @name Two.Texture#_offset
* @private
* @see {@link Two.Texture#offset}
*/
__publicField(this, "_offset", null);
for (let prop in proto4) {
Object.defineProperty(this, prop, proto4[prop]);
}
this._renderer.type = "texture";
this._renderer.flagOffset = FlagOffset.bind(this);
this._renderer.flagScale = FlagScale.bind(this);
this.loaded = false;
this.repeat = "no-repeat";
this.offset = new Vector();
if (typeof callback === "function") {
const loaded = function() {
this.unbind(Events.Types.load, loaded);
if (typeof callback === "function") {
callback();
}
}.bind(this);
this.bind(Events.Types.load, loaded);
}
if (typeof src === "string") {
this.src = src;
} else if (typeof src === "object") {
const elemString = Object.prototype.toString.call(src);
if (elemString === "[object HTMLImageElement]" || elemString === "[object HTMLCanvasElement]" || elemString === "[object HTMLVideoElement]" || elemString === "[object Image]") {
this.image = src;
}
}
this._update();
}
/**
* @name Two.Texture.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Texture} to create a new instance
* @returns {Two.Texture}
* @description Create a new {@link Two.Texture} from an object notation of a {@link Two.Texture}.
* @nota-bene Works in conjunction with {@link Two.Texture#toObject}
*/
fromObject(obj) {
const texture = new _Texture().copy(obj);
if ("id" in obj) {
texture.id = obj.id;
}
return texture;
}
/**
* @name Two.Texture.getAbsoluteURL
* @property {Function} - Serializes a URL as an absolute path for canonical attribution in {@link Two.Texture.ImageRegistry}.
* @param {String} path
* @returns {String} - The serialized absolute path.
*/
static getAbsoluteURL(path) {
if (!anchor) {
return path;
}
anchor.href = path;
return anchor.href;
}
/**
* @name Two.Texture.loadHeadlessBuffer
* @property {Function} - Loads an image as a buffer in headless environments.
* @param {Two.Texture} texture - The {@link Two.Texture} to be loaded.
* @param {Function} onLoad - The callback function to be triggered once the image is loaded.
* @nota-bene - This function uses node's `fs.readFileSync` to spoof the `` loading process in the browser.
*/
static loadHeadlessBuffer(texture, onLoad) {
texture.image.onload = onLoad;
texture.image.src = texture.src;
}
/**
* @name Two.Texture.getTag
* @property {Function} - Retrieves the tag name of an image, video, or canvas node.
* @param {HTMLImageElement} image - The image to infer the tag name from.
* @returns {String} - Returns the tag name of an image, video, or canvas node.
*/
static getTag(image) {
return image && image.nodeName && image.nodeName.toLowerCase() || // Headless environments
"img";
}
/**
* @name Two.Texture.getImage
* @property {Function} - Convenience function to set {@link Two.Texture#image} properties with canonical versions set in {@link Two.Texture.ImageRegistry}.
* @param {String} src - The URL path of the image.
* @returns {HTMLImageElement} - Returns either a cached version of the image or a new one that is registered in {@link Two.Texture.ImageRegistry}.
*/
static getImage(src) {
const absoluteSrc = _Texture.getAbsoluteURL(src);
if (_Texture.ImageRegistry.contains(absoluteSrc)) {
return _Texture.ImageRegistry.get(absoluteSrc);
}
let image;
if (CanvasPolyfill.Image) {
image = new CanvasPolyfill.Image();
CanvasPolyfill.shim(image, "img");
} else if (root.document) {
if (regex.video.test(absoluteSrc)) {
image = document.createElement("video");
} else {
image = document.createElement("img");
}
} else {
console.warn("Two.js: no prototypical image defined for Two.Texture");
}
image.crossOrigin = "anonymous";
image.referrerPolicy = "no-referrer";
return image;
}
/**
* @name Two.Texture.load
* @function
* @param {Two.Texture} texture - The texture to load.
* @param {Function} callback - The function to be called once the texture is loaded.
*/
static load(texture, callback) {
let image = texture.image;
let tag = _Texture.getTag(image);
if (texture._flagImage) {
if (/canvas/i.test(tag)) {
_Texture.Register.canvas(texture, callback);
} else {
texture._src = !CanvasPolyfill.isHeadless && image.getAttribute("two-src") || image.src;
_Texture.Register[tag](texture, callback);
}
}
if (texture._flagSrc) {
if (!image) {
image = _Texture.getImage(texture.src);
texture.image = image;
}
tag = _Texture.getTag(image);
_Texture.Register[tag](texture, callback);
}
}
/**
* @name Two.Texture#clone
* @function
* @returns {Two.Texture}
* @description Create a new instance of {@link Two.Texture} with the same properties of the current texture.
*/
clone() {
const clone = new _Texture(this.src);
clone.repeat = this.repeat;
clone.offset.copy(this.offset);
clone.scale = this.scale;
return clone;
}
/**
* @name Two.Texture#copy
* @function
* @param {Two.Texture} texture - The reference {@link Two.Texture}
* @description Copy the properties of one {@link Two.Texture} onto another.
*/
copy(texture) {
this.src = texture.src;
this.repeat = texture.repeat;
this.offset = typeof texture.offset === "number" || texture.offset instanceof Vector ? texture.offset : new Vector().copy(texture.offset);
this.scale = typeof texture.scale === "number" || texture.scale instanceof Vector ? texture.scale : new Vector().copy(texture.scale);
return this;
}
/**
* @name Two.Texture#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the texture.
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = "texture";
result.src = this.src;
result.repeat = this.repeat;
result.offset = this.offset.toObject();
result.scale = typeof this.scale === "number" ? this.scale : this.scale.toObject();
return result;
}
/**
* @name Two.Texture#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagSrc || this._flagImage) {
this.trigger(Events.Types.change);
if (this._flagSrc || this._flagImage) {
this.loaded = false;
_Texture.load(
this,
function() {
this.loaded = true;
this.trigger(Events.Types.change).trigger(Events.Types.load);
}.bind(this)
);
}
}
if (this._image && this._image.readyState >= 4) {
this._flagVideo = true;
}
return this;
}
/**
* @name Two.Texture#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagSrc = this._flagImage = this._flagLoaded = this._flagRepeat = this._flagVideo = this._flagScale = this._flagOffset = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Texture#dispose
* @function
* @description Detach instance from renderer including any `` or textures stored in memory.
*/
dispose() {
super.dispose();
if ("elem" in this._renderer) {
const elem = this._renderer.elem;
elem.parentNode.removeChild(elem);
}
if ("effect" in this._renderer) {
this._renderer.effect = null;
}
return this;
}
};
/**
* @name Two.Texture.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Texture}.
*/
__publicField(_Texture, "Properties", ["src", "loaded", "repeat", "scale", "offset", "image"]);
/**
* @name Two.Texture.RegularExpressions
* @property {Object} - A map of compatible DOM Elements categorized by media format.
*/
__publicField(_Texture, "RegularExpressions", regex);
/**
* @name Two.Texture.ImageRegistry
* @property {Two.Registry} - A canonical listing of image data used in a single session of Two.js.
* @nota-bene This object is used to cache image data between different textures.
*/
__publicField(_Texture, "ImageRegistry", new Registry());
/**
* @name Two.Texture.Register
* @interface
* @description A collection of functions to register different types of textures. Used internally by a {@link Two.Texture}.
*/
__publicField(_Texture, "Register", {
canvas: function(texture, callback) {
texture._src = "#" + texture.id;
_Texture.ImageRegistry.add(texture.src, texture.image);
if (typeof callback === "function") {
callback();
}
},
img: function(texture, callback) {
const image = texture.image;
const loaded = function(e) {
if (!CanvasPolyfill.isHeadless && image.removeEventListener && typeof image.removeEventListener === "function") {
image.removeEventListener("load", loaded, false);
image.removeEventListener("error", error, false);
}
if (typeof callback === "function") {
callback();
}
};
const error = function(e) {
if (!CanvasPolyfill.isHeadless && typeof image.removeEventListener === "function") {
image.removeEventListener("load", loaded, false);
image.removeEventListener("error", error, false);
}
throw new TwoError("unable to load " + texture.src);
};
if (typeof image.width === "number" && image.width > 0 && typeof image.height === "number" && image.height > 0) {
loaded();
} else if (!CanvasPolyfill.isHeadless && typeof image.addEventListener === "function") {
image.addEventListener("load", loaded, false);
image.addEventListener("error", error, false);
}
texture._src = _Texture.getAbsoluteURL(texture._src);
if (!CanvasPolyfill.isHeadless && image && image.getAttribute("two-src")) {
return;
}
if (!CanvasPolyfill.isHeadless) {
image.setAttribute("two-src", texture.src);
}
_Texture.ImageRegistry.add(texture.src, image);
if (CanvasPolyfill.isHeadless) {
_Texture.loadHeadlessBuffer(texture, loaded);
} else {
texture.image.src = texture.src;
}
},
video: function(texture, callback) {
if (CanvasPolyfill.isHeadless) {
throw new TwoError(
"video textures are not implemented in headless environments."
);
}
const loaded = function(e) {
texture.image.removeEventListener("canplaythrough", loaded, false);
texture.image.removeEventListener("error", error, false);
texture.image.width = texture.image.videoWidth;
texture.image.height = texture.image.videoHeight;
if (typeof callback === "function") {
callback();
}
};
const error = function(e) {
texture.image.removeEventListener("canplaythrough", loaded, false);
texture.image.removeEventListener("error", error, false);
throw new TwoError("unable to load " + texture.src);
};
texture._src = _Texture.getAbsoluteURL(texture._src);
if (!texture.image.getAttribute("two-src")) {
texture.image.setAttribute("two-src", texture.src);
_Texture.ImageRegistry.add(texture.src, texture.image);
}
if (texture.image.readyState >= 4) {
loaded();
} else {
texture.image.addEventListener("canplaythrough", loaded, false);
texture.image.addEventListener("error", error, false);
texture.image.src = texture.src;
texture.image.load();
}
}
});
var Texture = _Texture;
var proto4 = {
src: {
enumerable: true,
get: function() {
return this._src;
},
set: function(v) {
this._src = v;
this._flagSrc = true;
}
},
loaded: {
enumerable: true,
get: function() {
return this._loaded;
},
set: function(v) {
this._loaded = v;
this._flagLoaded = true;
}
},
repeat: {
enumerable: true,
get: function() {
return this._repeat;
},
set: function(v) {
this._repeat = v;
this._flagRepeat = true;
}
},
image: {
enumerable: true,
get: function() {
return this._image;
},
set: function(image) {
const tag = Texture.getTag(image);
let index;
switch (tag) {
case "canvas":
index = "#" + image.id;
break;
default:
index = image.src;
}
if (Texture.ImageRegistry.contains(index)) {
this._image = Texture.ImageRegistry.get(image.src);
} else {
this._image = image;
}
this._flagImage = true;
}
},
offset: {
enumerable: true,
get: function() {
return this._offset;
},
set: function(v) {
if (this._offset) {
this._offset.unbind(Events.Types.change, this._renderer.flagOffset);
}
this._offset = v;
this._offset.bind(Events.Types.change, this._renderer.flagOffset);
this._flagOffset = true;
}
},
scale: {
enumerable: true,
get: function() {
return this._scale;
},
set: function(v) {
if (this._scale instanceof Vector) {
this._scale.unbind(Events.Types.change, this._renderer.flagScale);
}
this._scale = v;
if (this._scale instanceof Vector) {
this._scale.bind(Events.Types.change, this._renderer.flagScale);
}
this._flagScale = true;
}
}
};
function FlagOffset() {
this._flagOffset = true;
}
function FlagScale() {
this._flagScale = true;
}
// src/effects/stop.js
var _Stop = class _Stop extends Element {
constructor(offset, color, opacity) {
super();
/**
* @name Two.Stop#_flagOffset
* @private
* @property {Boolean} - Determines whether the {@link Two.Stop#offset} needs updating.
*/
__publicField(this, "_flagOffset", true);
/**
* @name Two.Stop#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Stop#opacity} needs updating.
*/
__publicField(this, "_flagOpacity", true);
/**
* @name Two.Stop#_flagColor
* @private
* @property {Boolean} - Determines whether the {@link Two.Stop#color} needs updating.
*/
__publicField(this, "_flagColor", true);
/**
* @name Two.Stop#_offset
* @private
* @see {@link Two.Stop#offset}
*/
__publicField(this, "_offset", 0);
/**
* @name Two.Stop#_opacity
* @private
* @see {@link Two.Stop#opacity}
*/
__publicField(this, "_opacity", 1);
/**
* @name Two.Stop#_color
* @private
* @see {@link Two.Stop#color}
*/
__publicField(this, "_color", "#fff");
for (let prop in proto5) {
Object.defineProperty(this, prop, proto5[prop]);
}
this._renderer.type = "stop";
this.offset = typeof offset === "number" ? offset : _Stop.Index <= 0 ? 0 : 1;
this.opacity = typeof opacity === "number" ? opacity : 1;
this.color = typeof color === "string" ? color : _Stop.Index <= 0 ? "#fff" : "#000";
_Stop.Index = (_Stop.Index + 1) % 2;
}
/**
* @name Two.Stop.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Stop} to create a new instance
* @returns {Two.Stop}
* @description Create a new {@link Two.Stop} from an object notation of a {@link Two.Stop}.
* @nota-bene Works in conjunction with {@link Two.Stop#toObject}
*/
static fromObject(obj) {
const stop = new _Stop().copy(obj);
if ("id" in obj) {
stop.id = obj.id;
}
return stop;
}
/**
* @name Two.Stop#copy
* @function
* @param {Two.Stop} stop - The reference {@link Two.Stop}
* @description Copy the properties of one {@link Two.Stop} onto another.
*/
copy(stop) {
super.copy.call(this, stop);
for (let i = 0; i < _Stop.Properties.length; i++) {
const k = _Stop.Properties[i];
if (k in stop) {
this[k] = stop[k];
}
}
return this;
}
/**
* @name Two.Stop#clone
* @function
* @param {Two.Gradient} [parent] - The parent gradient to add the clone to.
* @returns {Two.Stop}
* @description Create a new instance of {@link Two.Stop} with the same properties of the current path.
*/
clone(parent) {
const clone = new _Stop();
_.each(
_Stop.Properties,
function(property) {
clone[property] = this[property];
},
this
);
if (parent && parent.stops) {
parent.stops.push(clone);
}
return clone;
}
/**
* @name Two.Stop#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = "stop";
_.each(
_Stop.Properties,
(k) => {
result[k] = this[k];
},
this
);
return result;
}
/**
* @name Two.Stop#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagOffset = this._flagColor = this._flagOpacity = false;
super.flagReset.call(this);
return this;
}
};
/**
* @name Two.Stop.Index
* @property {Number} - The current index being referenced for calculating a stop's default offset value.
*/
__publicField(_Stop, "Index", 0);
/**
* @name Two.Stop.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Stop}.
*/
__publicField(_Stop, "Properties", ["offset", "opacity", "color"]);
var Stop = _Stop;
var proto5 = {
offset: {
enumerable: true,
get: function() {
return this._offset;
},
set: function(v) {
this._offset = v;
this._flagOffset = true;
if (this.parent) {
this.parent._flagStops = true;
}
}
},
opacity: {
enumerable: true,
get: function() {
return this._opacity;
},
set: function(v) {
this._opacity = v;
this._flagOpacity = true;
if (this.parent) {
this.parent._flagStops = true;
}
}
},
color: {
enumerable: true,
get: function() {
return this._color;
},
set: function(v) {
this._color = v;
this._flagColor = true;
if (this.parent) {
this.parent._flagStops = true;
}
}
}
};
// src/effects/gradient.js
var _Gradient = class _Gradient extends Element {
constructor(stops) {
super();
__publicField(this, "_flagStops", false);
__publicField(this, "_flagSpread", false);
__publicField(this, "_flagUnits", false);
__publicField(this, "_spread", "");
__publicField(this, "_units", "");
for (let prop in proto6) {
Object.defineProperty(this, prop, proto6[prop]);
}
this._renderer.type = "gradient";
this._renderer.flagStops = FlagStops.bind(this);
this._renderer.bindStops = BindStops.bind(this);
this._renderer.unbindStops = UnbindStops.bind(this);
this.spread = "pad";
this.units = "objectBoundingBox";
if (stops) {
this.stops = stops;
}
}
/**
* @name Two.Gradient.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Gradient} to create a new instance
* @returns {Two.Gradient}
* @description Create a new {@link Two.Gradient} from an object notation of a {@link Two.Gradient}.
* @nota-bene Works in conjunction with {@link Two.Gradient#toObject}
*/
static fromObject(obj) {
let stops = obj.stops;
if (stops && stops.length > 0) {
stops = stops.map((o) => o instanceof Stop ? o : new Stop().copy(o));
}
const gradient = new _Gradient(stops).copy(obj);
if ("id" in obj) {
gradient.id = obj.id;
}
return gradient;
}
/**
* @name Two.Gradient#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Gradient}
* @description Create a new instance of {@link Two.Gradient} with the same properties of the current path.
*/
clone(parent) {
const stops = this.stops.map((s) => {
return s.clone();
});
const clone = new _Gradient(stops);
_.each(
_Gradient.Properties,
(k) => {
clone[k] = this[k];
},
this
);
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Gradient#copy
* @function
* @param {Two.Gradient} gradient - The reference {@link Two.Gradient}
* @description Copy the properties of one {@link Two.Gradient} onto another.
*/
copy(gradient) {
super.copy.call(this, gradient);
for (let i = 0; i < _Gradient.Properties.length; i++) {
const k = _Gradient.Properties[i];
if (k in gradient) {
this[k] = gradient[k];
}
}
return this;
}
/**
* @name Two.Gradient#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const result = {
stops: this.stops.map((s) => {
return s.toObject();
})
};
_.each(
_Gradient.Properties,
(k) => {
result[k] = this[k];
},
this
);
return result;
}
/**
* @name Two.Gradient#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagSpread || this._flagStops) {
this.trigger(Events.Types.change);
}
return this;
}
/**
* @name Two.Gradient#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagSpread = this._flagUnits = this._flagStops = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Gradient#dispose
* @function
* @description Detach instance from renderer including any `` or textures stored in memory.
*/
dispose() {
if ("elem" in this._renderer) {
const elem = this._renderer.elem;
elem.parentNode.removeChild(elem);
}
if ("effect" in this._renderer) {
this._renderer.effect = null;
}
return this;
}
};
/**
* @name Two.Gradient.Stop
* @see {@link Two.Stop}
*/
__publicField(_Gradient, "Stop", Stop);
/**
* @name Two.Gradient.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Gradient}.
*/
__publicField(_Gradient, "Properties", ["spread", "stops", "units"]);
var Gradient = _Gradient;
var proto6 = {
spread: {
enumerable: true,
get: function() {
return this._spread;
},
set: function(v) {
this._spread = v;
this._flagSpread = true;
}
},
units: {
enumerable: true,
get: function() {
return this._units;
},
set: function(v) {
this._units = v;
this._flagUnits = true;
}
},
stops: {
enumerable: true,
get: function() {
return this._stops;
},
set: function(stops) {
const bindStops = this._renderer.bindStops;
const unbindStops = this._renderer.unbindStops;
if (this._stops) {
this._stops.unbind(Events.Types.insert, bindStops).unbind(Events.Types.remove, unbindStops);
}
this._stops = new Collection((stops || []).slice(0));
this._stops.bind(Events.Types.insert, bindStops).bind(Events.Types.remove, unbindStops);
bindStops(this._stops);
}
}
};
function FlagStops() {
this._flagStops = true;
}
function BindStops(items) {
let i = items.length;
while (i--) {
items[i].bind(Events.Types.change, this._renderer.flagStops);
items[i].parent = this;
}
this._renderer.flagStops();
}
function UnbindStops(items) {
let i = items.length;
while (i--) {
items[i].unbind(Events.Types.change, this._renderer.flagStops);
delete items[i].parent;
}
this._renderer.flagStops();
}
// src/effects/linear-gradient.js
var _LinearGradient = class _LinearGradient extends Gradient {
constructor(x1, y1, x2, y2, stops) {
super(stops);
/**
* @name Two.LinearGradient#_flagEndPoints
* @private
* @property {Boolean} - Determines whether the {@link Two.LinearGradient#left} or {@link Two.LinearGradient#right} changed and needs to update.
*/
__publicField(this, "_flagEndPoints", false);
__publicField(this, "_left", null);
__publicField(this, "_right", null);
for (let prop in proto7) {
Object.defineProperty(this, prop, proto7[prop]);
}
this._renderer.type = "linear-gradient";
this._renderer.flagEndPoints = FlagEndPoints.bind(this);
this.left = new Vector();
this.right = new Vector();
if (typeof x1 === "number") {
this.left.x = x1;
}
if (typeof y1 === "number") {
this.left.y = y1;
}
if (typeof x2 === "number") {
this.right.x = x2;
}
if (typeof y2 === "number") {
this.right.y = y2;
}
}
/**
* @name Two.LinearGradient.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.LinearGradient} to create a new instance
* @returns {Two.LinearGradient}
* @description Create a new {@link Two.LinearGradient} from an object notation of a {@link Two.LinearGradient}.
* @nota-bene Works in conjunction with {@link Two.LinearGradient#toObject}
*/
static fromObject(obj) {
const gradient = new _LinearGradient().copy(obj);
if ("id" in obj) {
gradient.id = obj.id;
}
return gradient;
}
/**
* @name Two.LinearGradient#copy
* @function
* @param {Two.LinearGradient} gradient - The reference {@link Two.LinearGradient}
* @description Copy the properties of one {@link Two.LinearGradient} onto another.
*/
copy(gradient) {
super.copy.call(this, gradient);
for (let i = 0; i < _LinearGradient.Properties.length; i++) {
const k = _LinearGradient.Properties[i];
if (k in gradient) {
this[k] = gradient[k] instanceof Vector ? gradient[k] : new Vector().copy(gradient[k]);
}
}
return this;
}
/**
* @name Two.LinearGradient#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Gradient}
* @description Create a new instance of {@link Two.LinearGradient} with the same properties of the current path.
*/
clone(parent) {
const stops = this.stops.map(function(stop) {
return stop.clone();
});
const clone = new _LinearGradient(
this.left._x,
this.left._y,
this.right._x,
this.right._y,
stops
);
_.each(
Gradient.Properties,
function(k) {
clone[k] = this[k];
},
this
);
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.LinearGradient#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const result = super.toObject.call(this);
result.left = this.left.toObject();
result.right = this.right.toObject();
return result;
}
/**
* @name Two.LinearGradient#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagEndPoints || this._flagSpread || this._flagStops) {
this.trigger(Events.Types.change);
}
return this;
}
/**
* @name Two.LinearGradient#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagEndPoints = false;
super.flagReset.call(this);
return this;
}
};
/**
* @name Two.LinearGradient.Stop
* @see {@link Two.Stop}
*/
__publicField(_LinearGradient, "Stop", Stop);
__publicField(_LinearGradient, "Properties", ["left", "right"]);
var LinearGradient = _LinearGradient;
var proto7 = {
left: {
enumerable: true,
get: function() {
return this._left;
},
set: function(v) {
if (this._left instanceof Vector) {
this._left.unbind(Events.Types.change, this._renderer.flagEndPoints);
}
this._left = v;
this._left.bind(Events.Types.change, this._renderer.flagEndPoints);
this._flagEndPoints = true;
}
},
right: {
enumerable: true,
get: function() {
return this._right;
},
set: function(v) {
if (this._right instanceof Vector) {
this._right.unbind(Events.Types.change, this._renderer.flagEndPoints);
}
this._right = v;
this._right.bind(Events.Types.change, this._renderer.flagEndPoints);
this._flagEndPoints = true;
}
}
};
function FlagEndPoints() {
this._flagEndPoints = true;
}
// src/effects/radial-gradient.js
var _RadialGradient = class _RadialGradient extends Gradient {
constructor(cx, cy, r, stops, fx, fy) {
super(stops);
/**
* @name Two.RadialGradient#_flagRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.RadialGradient#radius} changed and needs to update.
*/
__publicField(this, "_flagRadius", false);
/**
* @name Two.RadialGradient#_flagCenter
* @private
* @property {Boolean} - Determines whether the {@link Two.RadialGradient#center} changed and needs to update.
*/
__publicField(this, "_flagCenter", false);
/**
* @name Two.RadialGradient#_flagFocal
* @private
* @property {Boolean} - Determines whether the {@link Two.RadialGradient#focal} changed and needs to update.
*/
__publicField(this, "_flagFocal", false);
__publicField(this, "_radius", 0);
__publicField(this, "_center", null);
__publicField(this, "_focal", null);
for (let prop in proto8) {
Object.defineProperty(this, prop, proto8[prop]);
}
this._renderer.type = "radial-gradient";
this._renderer.flagCenter = FlagCenter.bind(this);
this._renderer.flagFocal = FlagFocal.bind(this);
this.center = new Vector();
this.radius = typeof r === "number" ? r : 1;
this.focal = new Vector();
if (typeof cx === "number") {
this.center.x = cx;
}
if (typeof cy === "number") {
this.center.y = cy;
}
this.focal.copy(this.center);
if (typeof fx === "number") {
this.focal.x = fx;
}
if (typeof fy === "number") {
this.focal.y = fy;
}
}
/**
* @name Two.RadialGradient.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.RadialGradient} to create a new instance
* @returns {Two.RadialGradient}
* @description Create a new {@link Two.RadialGradient} from an object notation of a {@link Two.RadialGradient}.
* @nota-bene Works in conjunction with {@link Two.RadialGradient#toObject}
*/
static fromObject(obj) {
const gradient = new _RadialGradient().copy(obj);
if ("id" in obj) {
gradient.id = obj.id;
}
return gradient;
}
/**
* @name Two.RadialGradient#copy
* @function
* @param {Two.RadialGradient} gradient - The reference {@link Two.RadialGradient}
* @description Copy the properties of one {@link Two.RadialGradient} onto another.
*/
copy(gradient) {
super.copy.call(this, gradient);
for (let i = 0; i < _RadialGradient.Properties.length; i++) {
const k = _RadialGradient.Properties[i];
if (k in gradient) {
if (/(center|focal)i/.test(k)) {
this[k] = gradient[k] instanceof Vector ? gradient[k] : new Vector().copy(gradient[k]);
} else if (typeof gradient[k] === "number") {
this[k] = gradient[MediaKeySystemAccess];
}
}
}
return this;
}
/**
* @name Two.RadialGradient#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.RadialGradient}
* @description Create a new instance of {@link Two.RadialGradient} with the same properties of the current path.
*/
clone(parent) {
const stops = this.stops.map(function(stop) {
return stop.clone();
});
const clone = new _RadialGradient(
this.center._x,
this.center._y,
this._radius,
stops,
this.focal._x,
this.focal._y
);
_.each(
Gradient.Properties.concat(_RadialGradient.Properties),
function(k) {
clone[k] = this[k];
},
this
);
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.RadialGradient#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const result = super.toObject.call(this);
_.each(
_RadialGradient.Properties,
function(k) {
result[k] = this[k];
},
this
);
result.center = this.center.toObject();
result.focal = this.focal.toObject();
return result;
}
/**
* @name Two.RadialGradient#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagRadius || this._flatCenter || this._flagFocal || this._flagSpread || this._flagStops) {
this.trigger(Events.Types.change);
}
return this;
}
/**
* @name Two.RadialGradient#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagRadius = this._flagCenter = this._flagFocal = false;
super.flagReset.call(this);
return this;
}
};
/**
* @name Two.RadialGradient.Stop
* @see {@link Two.Stop}
*/
__publicField(_RadialGradient, "Stop", Stop);
/**
* @name Two.RadialGradient.Properties
* @property {String[]} - A list of properties that are on every {@link Two.RadialGradient}.
*/
__publicField(_RadialGradient, "Properties", ["center", "radius", "focal"]);
var RadialGradient = _RadialGradient;
var proto8 = {
radius: {
enumerable: true,
get: function() {
return this._radius;
},
set: function(v) {
this._radius = v;
this._flagRadius = true;
}
},
center: {
enumerable: true,
get: function() {
return this._center;
},
set: function(v) {
if (this._center) {
this._center.unbind(Events.Types.change, this._renderer.flagCenter);
}
this._center = v;
this._center.bind(Events.Types.change, this._renderer.flagCenter);
this._flagCenter = true;
}
},
focal: {
enumerable: true,
get: function() {
return this._focal;
},
set: function(v) {
if (this._focal) {
this._focal.unbind(Events.Types.change, this._renderer.flagFocal);
}
this._focal = v;
this._focal.bind(Events.Types.change, this._renderer.flagFocal);
this._flagFocal = true;
}
}
};
function FlagCenter() {
this._flagCenter = true;
}
function FlagFocal() {
this._flagFocal = true;
}
// src/utils/shape.js
function contains(path, t) {
if (t === 0 || t === 1) {
return true;
}
const length = path._length;
const target = length * t;
let elapsed = 0;
for (let i = 0; i < path._lengths.length; i++) {
const dist = path._lengths[i];
if (elapsed >= target) {
return target - elapsed >= 0;
}
elapsed += dist;
}
return false;
}
function getIdByLength(path, target) {
const total = path._length;
if (target <= 0) {
return 0;
} else if (target >= total) {
return path._lengths.length - 1;
}
for (let i = 0, sum = 0; i < path._lengths.length; i++) {
if (sum + path._lengths[i] >= target) {
target -= sum;
return Math.max(i - 1, 0) + target / path._lengths[i];
}
sum += path._lengths[i];
}
return -1;
}
function getCurveLength2(a, b, limit) {
let x1, x2, x3, x4, y1, y2, y3, y4;
const right = b.controls && b.controls.right;
const left = a.controls && a.controls.left;
x1 = b.x;
y1 = b.y;
x2 = (right || b).x;
y2 = (right || b).y;
x3 = (left || a).x;
y3 = (left || a).y;
x4 = a.x;
y4 = a.y;
if (right && b._relative) {
x2 += b.x;
y2 += b.y;
}
if (left && a._relative) {
x3 += a.x;
y3 += a.y;
}
return getCurveLength(x1, y1, x2, y2, x3, y3, x4, y4, limit);
}
function getSubdivisions(a, b, limit) {
let x1, x2, x3, x4, y1, y2, y3, y4;
const right = b.controls && b.controls.right;
const left = a.controls && a.controls.left;
x1 = b.x;
y1 = b.y;
x2 = (right || b).x;
y2 = (right || b).y;
x3 = (left || a).x;
y3 = (left || a).y;
x4 = a.x;
y4 = a.y;
if (right && b._relative) {
x2 += b.x;
y2 += b.y;
}
if (left && a._relative) {
x3 += a.x;
y3 += a.y;
}
return subdivide(x1, y1, x2, y2, x3, y3, x4, y4, limit);
}
function getEffectFromObject(obj) {
switch (obj.renderer.type) {
case "texture":
return Texture.fromObject(obj);
case "gradient":
return Gradient.fromObject(obj);
case "linear-gradient":
return LinearGradient.fromObject(obj);
case "radial-gradient":
return RadialGradient.fromObject(obj);
}
return obj;
}
// src/matrix.js
var cos = Math.cos;
var sin = Math.sin;
var tan = Math.tan;
var array = [];
var _Matrix = class _Matrix extends Events {
constructor(a, b, c, d, e, f) {
super();
/**
* @name Two.Matrix#elements
* @property {Number[]} - The underlying data stored as an array.
*/
__publicField(this, "elements", new NumArray(9));
/**
* @name Two.Matrix#manual
* @property {Boolean} - Determines whether Two.js automatically calculates the values for the matrix or if the developer intends to manage the matrix.
* @nota-bene - Setting to `true` nullifies {@link Two.Shape#translation}, {@link Two.Shape#rotation}, and {@link Two.Shape#scale}.
*/
__publicField(this, "manual", false);
let elements = a;
if (!Array.isArray(elements)) {
elements = Array.prototype.slice.call(arguments);
}
this.identity();
if (elements.length > 0) {
this.set(elements);
}
}
/**
* @name Two.Matrix.Multiply
* @function
* @param {Number[]} A - The first {@link Two.Matrix} to multiply
* @param {Number[]} B - The second {@link Two.Matrix} to multiply
* @param {Number[]} [C] - An optional {@link Two.Matrix} to apply the result to
* @returns {Number[]} - If an optional `C` matrix isn't passed then a new one is created and returned.
* @description Multiply two matrices together and return the result.
*/
static Multiply(A, B, C) {
if (B.length <= 3) {
const e = A;
let x, y, z;
const a = B[0] || 0, b = B[1] || 0, c = B[2] || 0;
x = e[0] * a + e[1] * b + e[2] * c;
y = e[3] * a + e[4] * b + e[5] * c;
z = e[6] * a + e[7] * b + e[8] * c;
return [x, y, z];
}
const A0 = A[0], A1 = A[1], A2 = A[2];
const A3 = A[3], A4 = A[4], A5 = A[5];
const A6 = A[6], A7 = A[7], A8 = A[8];
const B0 = B[0], B1 = B[1], B2 = B[2];
const B3 = B[3], B4 = B[4], B5 = B[5];
const B6 = B[6], B7 = B[7], B8 = B[8];
C = C || new NumArray(9);
C[0] = A0 * B0 + A1 * B3 + A2 * B6;
C[1] = A0 * B1 + A1 * B4 + A2 * B7;
C[2] = A0 * B2 + A1 * B5 + A2 * B8;
C[3] = A3 * B0 + A4 * B3 + A5 * B6;
C[4] = A3 * B1 + A4 * B4 + A5 * B7;
C[5] = A3 * B2 + A4 * B5 + A5 * B8;
C[6] = A6 * B0 + A7 * B3 + A8 * B6;
C[7] = A6 * B1 + A7 * B4 + A8 * B7;
C[8] = A6 * B2 + A7 * B5 + A8 * B8;
return C;
}
/**
* @name Two.Matrix.fromObject
* @function
* @param {Object} obj - The object notation of a Two.Matrix to create a new instance
* @returns {Two.Matrix}
* @description Create a new {@link Two.Matrix} from an object notation of a {@link Two.Matrix}.
* @nota-bene Works in conjunction with {@link Two.Matrix#toObject}
*/
static fromObject(obj) {
return new _Matrix().copy(obj);
}
/**
* @name Two.Matrix#set
* @function
* @param {Number} a - The value for element at the first column and first row
* @param {Number} b - The value for element at the second column and first row
* @param {Number} c - The value for element at the third column and first row
* @param {Number} d - The value for element at the first column and second row
* @param {Number} e - The value for element at the second column and second row
* @param {Number} f - The value for element at the third column and second row
* @param {Number} g - The value for element at the first column and third row
* @param {Number} h - The value for element at the second column and third row
* @param {Number} i - The value for element at the third column and third row
* @description Set an array of values onto the matrix. Order described in {@link Two.Matrix}.
*/
/**
* @name Two.Matrix#set
* @function
* @param {Number[]} a - The array of elements to apply
* @description Set an array of values onto the matrix. Order described in {@link Two.Matrix}.
*/
set(a, b, c, d, e, f, g, h, i) {
if (typeof b === "undefined") {
const elements = a;
a = elements[0];
b = elements[1];
c = elements[2];
d = elements[3];
e = elements[4];
f = elements[5];
g = elements[6];
h = elements[7];
i = elements[8];
}
this.elements[0] = a;
this.elements[1] = b;
this.elements[2] = c;
this.elements[3] = d;
this.elements[4] = e;
this.elements[5] = f;
this.elements[6] = g;
this.elements[7] = h;
this.elements[8] = i;
return this.trigger(Events.Types.change);
}
/**
* @name Two.Matrix#copy
* @function
* @param {Two.Matrix} m - The matrix to copy
* @description Copy the matrix of one to the current instance.
*/
copy(m) {
this.elements[0] = m.elements[0];
this.elements[1] = m.elements[1];
this.elements[2] = m.elements[2];
this.elements[3] = m.elements[3];
this.elements[4] = m.elements[4];
this.elements[5] = m.elements[5];
this.elements[6] = m.elements[6];
this.elements[7] = m.elements[7];
this.elements[8] = m.elements[8];
this.manual = m.manual;
return this.trigger(Events.Types.change);
}
/**
* @name Two.Matrix#identity
* @function
* @description Turn matrix to the identity, like resetting.
*/
identity() {
this.elements[0] = _Matrix.Identity[0];
this.elements[1] = _Matrix.Identity[1];
this.elements[2] = _Matrix.Identity[2];
this.elements[3] = _Matrix.Identity[3];
this.elements[4] = _Matrix.Identity[4];
this.elements[5] = _Matrix.Identity[5];
this.elements[6] = _Matrix.Identity[6];
this.elements[7] = _Matrix.Identity[7];
this.elements[8] = _Matrix.Identity[8];
return this.trigger(Events.Types.change);
}
/**
* @name Two.Matrix#multiply
* @function
* @param {Number} s - The scalar to be multiplied.
* @description Multiply all components of the matrix against a single scalar value.
* @overloaded
*/
/**
* @name Two.Matrix#multiply
* @function
* @param {Number} x - The `x` component to be multiplied.
* @param {Number} y - The `y` component to be multiplied.
* @param {Number} z - The `z` component to be multiplied.
* @description Multiply all components of a matrix against a 3 component vector.
* @overloaded
*/
/**
* @name Two.Matrix#multiply
* @function
* @param {Number} a - The value at the first column and first row of the matrix to be multiplied.
* @param {Number} b - The value at the second column and first row of the matrix to be multiplied.
* @param {Number} c - The value at the third column and first row of the matrix to be multiplied.
* @param {Number} d - The value at the first column and second row of the matrix to be multiplied.
* @param {Number} e - The value at the second column and second row of the matrix to be multiplied.
* @param {Number} f - The value at the third column and second row of the matrix to be multiplied.
* @param {Number} g - The value at the first column and third row of the matrix to be multiplied.
* @param {Number} h - The value at the second column and third row of the matrix to be multiplied.
* @param {Number} i - The value at the third column and third row of the matrix to be multiplied.
* @description Multiply all components of a matrix against another matrix.
* @overloaded
*/
multiply(a, b, c, d, e, f, g, h, i) {
if (typeof b === "undefined") {
this.elements[0] *= a;
this.elements[1] *= a;
this.elements[2] *= a;
this.elements[3] *= a;
this.elements[4] *= a;
this.elements[5] *= a;
this.elements[6] *= a;
this.elements[7] *= a;
this.elements[8] *= a;
return this.trigger(Events.Types.change);
}
if (typeof c === "undefined") {
c = 1;
}
if (typeof d === "undefined") {
a = a || 0;
b = b || 0;
c = c || 0;
e = this.elements;
const x = e[0] * a + e[1] * b + e[2] * c;
const y = e[3] * a + e[4] * b + e[5] * c;
const z = e[6] * a + e[7] * b + e[8] * c;
return [x, y, z];
}
const A = this.elements;
const B = [a, b, c, d, e, f, g, h, i];
const A0 = A[0], A1 = A[1], A2 = A[2];
const A3 = A[3], A4 = A[4], A5 = A[5];
const A6 = A[6], A7 = A[7], A8 = A[8];
const B0 = B[0], B1 = B[1], B2 = B[2];
const B3 = B[3], B4 = B[4], B5 = B[5];
const B6 = B[6], B7 = B[7], B8 = B[8];
this.elements[0] = A0 * B0 + A1 * B3 + A2 * B6;
this.elements[1] = A0 * B1 + A1 * B4 + A2 * B7;
this.elements[2] = A0 * B2 + A1 * B5 + A2 * B8;
this.elements[3] = A3 * B0 + A4 * B3 + A5 * B6;
this.elements[4] = A3 * B1 + A4 * B4 + A5 * B7;
this.elements[5] = A3 * B2 + A4 * B5 + A5 * B8;
this.elements[6] = A6 * B0 + A7 * B3 + A8 * B6;
this.elements[7] = A6 * B1 + A7 * B4 + A8 * B7;
this.elements[8] = A6 * B2 + A7 * B5 + A8 * B8;
return this.trigger(Events.Types.change);
}
/**
* @name Two.Matrix#inverse
* @function
* @param {Two.Matrix} [output] - The optional matrix to apply the inversion to.
* @description Return an inverted version of the matrix. If no optional one is passed a new matrix is created and returned.
*/
inverse(output) {
const a = this.elements;
output = output || new _Matrix();
const a00 = a[0], a01 = a[1], a02 = a[2];
const a10 = a[3], a11 = a[4], a12 = a[5];
const a20 = a[6], a21 = a[7], a22 = a[8];
const b01 = a22 * a11 - a12 * a21;
const b11 = -a22 * a10 + a12 * a20;
const b21 = a21 * a10 - a11 * a20;
let det = a00 * b01 + a01 * b11 + a02 * b21;
if (!det) {
return null;
}
det = 1 / det;
output.elements[0] = b01 * det;
output.elements[1] = (-a22 * a01 + a02 * a21) * det;
output.elements[2] = (a12 * a01 - a02 * a11) * det;
output.elements[3] = b11 * det;
output.elements[4] = (a22 * a00 - a02 * a20) * det;
output.elements[5] = (-a12 * a00 + a02 * a10) * det;
output.elements[6] = b21 * det;
output.elements[7] = (-a21 * a00 + a01 * a20) * det;
output.elements[8] = (a11 * a00 - a01 * a10) * det;
return output;
}
/**
* @name Two.Matrix#scale
* @function
* @param {Number} s - The one dimensional scale to apply to the matrix.
* @description Uniformly scale the transformation matrix.
*/
/**
* @name Two.Matrix#scale
* @function
* @param {Number} sx - The horizontal scale factor.
* @param {Number} sy - The vertical scale factor
* @description Scale the transformation matrix in two dimensions.
*/
scale(sx, sy) {
const l = arguments.length;
if (l <= 1) {
sy = sx;
}
return this.multiply(sx, 0, 0, 0, sy, 0, 0, 0, 1);
}
/**
* @name Two.Matrix#rotate
* @function
* @param {Number} n - The amount to rotate in Number.
* @description Rotate the matrix.
*/
rotate(n) {
const c = cos(n);
const s = sin(n);
return this.multiply(c, -s, 0, s, c, 0, 0, 0, 1);
}
/**
* @name Two.Matrix#translate
* @function
* @param {Number} x - The horizontal translation value to apply
* @param {Number} y - The vertical translation value to apply
* @description Translate the matrix to specific `x` / `y` values.
*/
translate(x, y) {
return this.multiply(1, 0, x, 0, 1, y, 0, 0, 1);
}
/**
* @name Two.Matrix#skewX
* @function
* @param {Number} n - The amount to skew
* @description Skew the matrix by an angle in the x axis direction.
*/
skewX(n) {
const a = tan(n);
return this.multiply(1, a, 0, 0, 1, 0, 0, 0, 1);
}
/**
* @name Two.Matrix#skewY
* @function
* @param {Number} n - The amount to skew
* @description Skew the matrix by an angle in the y axis direction.
*/
skewY(n) {
const a = tan(n);
return this.multiply(1, 0, 0, a, 1, 0, 0, 0, 1);
}
/**
* @name Two.Matrix#toString
* @function
* @param {Boolean} [fullMatrix=false] - Return the full 9 elements of the matrix or just 6 for 2D transformations.
* @returns {String} - The transformation matrix as a 6 component string separated by spaces.
* @description Create a transform string. Used for the Two.js rendering APIs.
*/
toString(fullMatrix) {
array.length = 0;
this.toTransformArray(fullMatrix, array);
return array.map(toFixed).join(" ");
}
/**
* @name Two.Matrix#toTransformArray
* @function
* @param {Boolean} [fullMatrix=false] - Return the full 9 elements of the matrix or just 6 in the format for 2D transformations.
* @param {Number[]} [output] - An array empty or otherwise to apply the values to.
* @description Create a transform array. Used for the Two.js rendering APIs.
*/
toTransformArray(fullMatrix, output) {
const elements = this.elements;
const hasOutput = !!output;
const a = elements[0];
const b = elements[1];
const c = elements[2];
const d = elements[3];
const e = elements[4];
const f = elements[5];
if (fullMatrix) {
const g = elements[6];
const h = elements[7];
const i = elements[8];
if (hasOutput) {
output[0] = a;
output[1] = d;
output[2] = g;
output[3] = b;
output[4] = e;
output[5] = h;
output[6] = c;
output[7] = f;
output[8] = i;
return;
}
return [a, d, g, b, e, h, c, f, i];
}
if (hasOutput) {
output[0] = a;
output[1] = d;
output[2] = b;
output[3] = e;
output[4] = c;
output[5] = f;
return;
}
return [
a,
d,
b,
e,
c,
f
// Specific format see LN:19
];
}
/**
* @name Two.Matrix#toArray
* @function
* @param {Boolean} [fullMatrix=false] - Return the full 9 elements of the matrix or just 6 for 2D transformations.
* @param {Number[]} [output] - An array empty or otherwise to apply the values to.
* @description Create a transform array. Used for the Two.js rendering APIs.
*/
toArray(fullMatrix, output) {
const elements = this.elements;
const hasOutput = !!output;
const a = elements[0];
const b = elements[1];
const c = elements[2];
const d = elements[3];
const e = elements[4];
const f = elements[5];
if (fullMatrix) {
const g = elements[6];
const h = elements[7];
const i = elements[8];
if (hasOutput) {
output[0] = a;
output[1] = b;
output[2] = c;
output[3] = d;
output[4] = e;
output[5] = f;
output[6] = g;
output[7] = h;
output[8] = i;
return;
}
return [a, b, c, d, e, f, g, h, i];
}
if (hasOutput) {
output[0] = a;
output[1] = b;
output[2] = c;
output[3] = d;
output[4] = e;
output[5] = f;
return;
}
return [a, b, c, d, e, f];
}
/**
* @name Two.Matrix#toObject
* @function
* @description Create a JSON compatible object that represents information of the matrix.
* @nota-bene Works in conjunction with {@link Two.Matrix.fromObject}
*/
toObject() {
return {
renderer: { type: "matrix" },
elements: this.toArray(true),
manual: !!this.manual
};
}
/**
* @name Two.Matrix#clone
* @function
* @description Clone the current matrix.
*/
clone() {
return new _Matrix().copy(this);
}
};
//
/**
* @name Two.Matrix.Identity
* @property {Number[]} - A stored reference to the default value of a 3 x 3 matrix.
*/
__publicField(_Matrix, "Identity", [1, 0, 0, 0, 1, 0, 0, 0, 1]);
var Matrix2 = _Matrix;
setMatrix(Matrix2);
// src/shape.js
var _Shape = class _Shape extends Element {
constructor() {
super();
/**
* @name Two.Shape#_flagMatrix
* @private
* @property {Boolean} - Determines whether the matrix needs updating.
*/
__publicField(this, "_flagMatrix", true);
/**
* @name Two.Shape#_flagScale
* @private
* @property {Boolean} - Determines whether the scale needs updating.
*/
__publicField(this, "_flagScale", false);
// Underlying Properties
/**
* @name Two.Shape#_matrix
* @private
* @property {Two.Matrix} - The matrix value of the shape's position, rotation, and scale.
*/
__publicField(this, "_matrix", null);
/**
* @name Two.Shape#_worldMatrix
* @private
* @property {Two.Matrix} - The matrix value of the shape's position, rotation, and scale in the scene.
*/
__publicField(this, "_worldMatrix", null);
/**
* @name Two.Shape#_position
* @private
* @property {Two.Vector} - The translation values as a {@link Two.Vector}.
*/
__publicField(this, "_position", null);
/**
* @name Two.Shape#_rotation
* @private
* @property {Number} - The rotation value in radians.
*/
__publicField(this, "_rotation", 0);
/**
* @name Two.Shape#_scale
* @private
* @property {Number|Two.Vector} - The scale value in Number. Can be a vector for non-uniform scaling.
*/
__publicField(this, "_scale", 1);
/**
* @name Two.Shape#_skewX
* @private
* @property {Number} - The rotation value in Number.
*/
__publicField(this, "_skewX", 0);
/**
* @name Two.Shape#_skewY
* @private
* @property {Number} - The rotation value in Number.
*/
__publicField(this, "_skewY", 0);
for (let prop in proto9) {
Object.defineProperty(this, prop, proto9[prop]);
}
this._renderer.flagMatrix = FlagMatrix.bind(this);
this.isShape = true;
this.matrix = new Matrix2();
this.worldMatrix = new Matrix2();
this.position = new Vector();
this.rotation = 0;
this.scale = 1;
this.skewX = 0;
this.skewY = 0;
}
/**
* @name Two.Shape.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Shape} to create a new instance
* @returns {Two.Shape}
* @description Create a new {@link Two.Shape} from an object notation of a {@link Two.Shape}.
* @nota-bene Works in conjunction with {@link Two.Shape#toObject}
*/
static fromObject(obj) {
const shape = new _Shape().copy(obj);
if ("id" in obj) {
shape.id = obj.id;
}
return shape;
}
get renderer() {
return this._renderer;
}
set renderer(v) {
this._renderer = v;
}
/**
* @name Two.Shape#translation
* @description Alias for {@link Two.Shape#position}.
*/
get translation() {
return proto9.position.get.apply(this, arguments);
}
set translation(v) {
proto9.position.set.apply(this, arguments);
}
/**
* @name Two.Shape#addTo
* @function
* @param {Two.Group} group - The parent the shape adds itself to.
* @description Convenience method to add itself to the scenegraph.
*/
addTo(group) {
group.add(this);
return this;
}
/**
* @name Two.Shape#remove
* @function
* @description Remove self from the scene / parent.
*/
remove() {
if (!this.parent) {
return this;
}
this.parent.remove(this);
return this;
}
/**
* @name Two.Shape#contains
* @function
* @param {Number} x - x coordinate to hit test against
* @param {Number} y - y coordinate to hit test against
* @param {Object} [options] - Optional options object
* @param {Boolean} [options.ignoreVisibility] - If `true`, hit test against `shape.visible = false` shapes
* @param {Number} [options.tolerance] - Padding to hit test against in pixels
* @returns {Boolean}
* @description Check to see if coordinates are within a {@link Two.Shape}'s bounding rectangle
* @nota-bene Expects *world-space coordinates* – the same pixel-space you get from the renderer (e.g., mouse `clientX`/`clientY` adjusted for the canvas’s offset and pixel ratio).
*/
contains(x, y, options) {
const opts = options || {};
const ignoreVisibility = opts.ignoreVisibility === true;
if (!ignoreVisibility && "visible" in this && this.visible === false) {
return false;
}
if (!ignoreVisibility && "opacity" in this && typeof this.opacity === "number" && this.opacity <= 0) {
return false;
}
if (typeof this.getBoundingClientRect !== "function") {
return false;
}
const tolerance = typeof opts.tolerance === "number" ? opts.tolerance : 0;
this._update(true);
const rect = this.getBoundingClientRect();
if (!rect) {
return false;
}
return x >= rect.left - tolerance && x <= rect.right + tolerance && y >= rect.top - tolerance && y <= rect.bottom + tolerance;
}
/**
* @name Two.Shape#copy
* @function
* @param {Two.Shape} shape
* @description Copy the properties of one {@link Two.Shape} onto another.
*/
copy(shape) {
super.copy.call(this, shape);
if ("position" in shape) {
if (shape.position instanceof Vector) {
this.position = shape.position;
} else {
this.position.copy(shape.position);
}
}
if ("rotation" in shape) {
this.rotation = shape.rotation;
}
if ("scale" in shape) {
this.scale = typeof shape.scale === "number" || shape.scale instanceof Vector ? shape.scale : new Vector(shape.scale.x, shape.scale.y);
}
if ("skewX" in shape) {
this.skewX = shape.skewX;
}
if ("skewY" in shape) {
this.skewY = shape.skewY;
}
if ("matrix" in shape && shape.matrix.manual) {
this.matrix.copy(shape.matrix);
this.matrix.manual = true;
}
return this;
}
/**
* @name Two.Shape#clone
* @function
* @param {Two.Group} [parent] - Optional argument to automatically add the shape to a scenegraph.
* @returns {Two.Shape}
* @description Create a new {@link Two.Shape} with the same values as the current shape.
*/
clone(parent) {
const clone = new _Shape();
clone.position.copy(this.position);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
if (parent) {
parent.add(clone);
}
return clone._update();
}
/**
* @name Two.Shape#toObject
* @function
* @description Create a JSON compatible object that represents information of the shape.
* @nota-bene Works in conjunction with {@link Two.Shape.fromObject}
*/
toObject() {
const result = super.toObject.call(this);
result.renderer = { type: "shape" };
result.isShape = true;
result.translation = this.translation.toObject();
result.rotation = this.translation.rotation;
result.scale = this.scale instanceof Vector ? this.scale.toObject() : this.scale;
result.skewX = this.skewX;
result.skewY = this.skewY;
result.matrix = this.matrix.toObject();
return result;
}
/**
* @name Two.Shape#dispose
* @function
* @description Release the shape's bound objects by unbinding relevant events.
*/
dispose() {
super.dispose();
if (typeof this.translation === "object" && typeof this.translation.unbind === "function") {
this.translation.unbind();
}
if (typeof this.scale === "object" && typeof this.scale.unbind === "function") {
this.scale.unbind();
}
}
/**
* @name Two.Shape#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update(bubbles) {
if (!this._matrix.manual && this._flagMatrix) {
this._matrix.identity().translate(this.position.x, this.position.y);
this._matrix.rotate(this.rotation);
if (this._scale instanceof Vector) {
this._matrix.scale(this._scale.x, this._scale.y);
} else {
this._matrix.scale(this._scale);
}
this._matrix.skewX(this.skewX);
this._matrix.skewY(this.skewY);
}
if (bubbles) {
if (this.parent && this.parent._update) {
this.parent._update();
}
}
return this;
}
/**
* @name Two.Shape#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagMatrix = this._flagScale = false;
super.flagReset.call(this);
return this;
}
};
__publicField(_Shape, "Properties", [
"position",
"rotation",
"scale",
"skewX",
"skewY",
"matrix",
"worldMatrix"
]);
var Shape = _Shape;
var proto9 = {
position: {
enumerable: true,
get: function() {
return this._position;
},
set: function(v) {
if (this._position) {
this._position.unbind(Events.Types.change, this._renderer.flagMatrix);
}
this._position = v;
this._position.bind(Events.Types.change, this._renderer.flagMatrix);
FlagMatrix.call(this);
}
},
rotation: {
enumerable: true,
get: function() {
return this._rotation;
},
set: function(v) {
this._rotation = v;
this._flagMatrix = true;
}
},
scale: {
enumerable: true,
get: function() {
return this._scale;
},
set: function(v) {
if (this._scale instanceof Vector) {
this._scale.unbind(Events.Types.change, this._renderer.flagMatrix);
}
this._scale = v;
if (this._scale instanceof Vector) {
this._scale.bind(Events.Types.change, this._renderer.flagMatrix);
}
this._flagMatrix = true;
this._flagScale = true;
}
},
skewX: {
enumerable: true,
get: function() {
return this._skewX;
},
set: function(v) {
this._skewX = v;
this._flagMatrix = true;
}
},
skewY: {
enumerable: true,
get: function() {
return this._skewY;
},
set: function(v) {
this._skewY = v;
this._flagMatrix = true;
}
},
matrix: {
enumerable: true,
get: function() {
return this._matrix;
},
set: function(v) {
this._matrix = v;
this._flagMatrix = true;
}
},
worldMatrix: {
enumerable: true,
get: function() {
getComputedMatrix(this, this._worldMatrix);
return this._worldMatrix;
},
set: function(v) {
this._worldMatrix = v;
}
}
};
function FlagMatrix() {
this._flagMatrix = true;
}
// src/utils/hit-test.js
var TRANSPARENT_REGEX = /^(?:none|transparent)$/i;
var DEFAULT_PRECISION = 8;
var EPSILON = Number.EPSILON;
function createPoint(x, y) {
return { x, y };
}
function pointsEqual(a, b, epsilon = EPSILON) {
return Math.abs(a.x - b.x) <= epsilon && Math.abs(a.y - b.y) <= epsilon;
}
function svgAngle(ux, uy, vx, vy) {
const dot = ux * vx + uy * vy;
const len = Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy) || 1e-12;
let ang = Math.acos(Math.max(-1, Math.min(1, dot / len)));
if (ux * vy - uy * vx < 0) {
ang = -ang;
}
return ang;
}
function sampleArcPoints(prev, anchor2, precision) {
if (!prev) {
return [createPoint(anchor2.x, anchor2.y)];
}
let rx = anchor2.rx;
let ry = anchor2.ry;
if (!(rx && ry)) {
return [createPoint(anchor2.x, anchor2.y)];
}
const xAxisRotation = (anchor2.xAxisRotation || 0) * Math.PI / 180;
const largeArcFlag = anchor2.largeArcFlag ? 1 : 0;
const sweepFlag = anchor2.sweepFlag ? 1 : 0;
rx = Math.abs(rx);
ry = Math.abs(ry);
const ax = prev.x;
const ay = prev.y;
const x = anchor2.x;
const y = anchor2.y;
const dx2 = (ax - x) / 2;
const dy2 = (ay - y) / 2;
const cosRot = Math.cos(xAxisRotation);
const sinRot = Math.sin(xAxisRotation);
let x1p = cosRot * dx2 + sinRot * dy2;
let y1p = -sinRot * dx2 + cosRot * dy2;
let rxs = rx * rx;
let rys = ry * ry;
const cr = x1p * x1p / rxs + y1p * y1p / rys;
if (cr > 1) {
const s = Math.sqrt(cr);
rx *= s;
ry *= s;
rxs = rx * rx;
rys = ry * ry;
}
const dq = rxs * y1p * y1p + rys * x1p * x1p;
const pq = dq === 0 ? 0 : (rxs * rys - dq) / dq;
let q = Math.sqrt(Math.max(0, pq));
if (largeArcFlag === sweepFlag) {
q = -q;
}
const cxp = q * rx * y1p / ry;
const cyp = -q * ry * x1p / rx;
const cx = cosRot * cxp - sinRot * cyp + (ax + x) / 2;
const cy = sinRot * cxp + cosRot * cyp + (ay + y) / 2;
const startAngle = svgAngle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
const delta = svgAngle(
(x1p - cxp) / rx,
(y1p - cyp) / ry,
(-x1p - cxp) / rx,
(-y1p - cyp) / ry
) % TWO_PI;
const endAngle = startAngle + delta;
const clockwise = sweepFlag === 0;
const angleDelta = (() => {
const raw = endAngle - startAngle;
const samePoints = Math.abs(raw) < Number.EPSILON;
let deltaAngle = mod(raw, TWO_PI);
if (deltaAngle < Number.EPSILON) {
deltaAngle = samePoints ? 0 : TWO_PI;
}
if (clockwise && !samePoints) {
deltaAngle = deltaAngle === TWO_PI ? -TWO_PI : deltaAngle - TWO_PI;
}
return deltaAngle;
})();
const steps = Math.max(Constants.Resolution, Math.max(precision * 2, 1));
const points = [];
for (let i = 1; i <= steps; i++) {
const t = i / steps;
const angle = startAngle + t * angleDelta;
let px = cx + rx * Math.cos(angle);
let py = cy + ry * Math.sin(angle);
if (xAxisRotation !== 0) {
const tx = px - cx;
const ty = py - cy;
const cosR = Math.cos(xAxisRotation);
const sinR = Math.sin(xAxisRotation);
px = tx * cosR - ty * sinR + cx;
py = tx * sinR + ty * cosR + cy;
}
points.push(createPoint(px, py));
}
return points;
}
function buildPathHitParts(path, precision = DEFAULT_PRECISION) {
const polygons = [];
const segments = [];
const vertices = path._renderer && path._renderer.vertices && path._renderer.vertices.length > 0 ? path._renderer.vertices : path.vertices;
if (!vertices || vertices.length === 0) {
return { polygons, segments };
}
const limit = Math.max(1, Math.floor(precision));
let currentPolygon = null;
let firstPoint = null;
let lastPoint = null;
let prevVertex = null;
const closePolygon = (forceClose = false) => {
if (!currentPolygon) {
return;
}
if (forceClose && firstPoint && lastPoint && !pointsEqual(firstPoint, lastPoint)) {
const closingPoint = createPoint(firstPoint.x, firstPoint.y);
segments.push({ a: lastPoint, b: closingPoint });
currentPolygon.push(closingPoint);
lastPoint = closingPoint;
}
if (currentPolygon.length >= 3 && firstPoint && lastPoint && pointsEqual(firstPoint, lastPoint)) {
polygons.push(currentPolygon);
}
currentPolygon = null;
firstPoint = null;
lastPoint = null;
};
const appendPoint = (pt) => {
if (!lastPoint) {
lastPoint = pt;
if (currentPolygon) {
currentPolygon.push(pt);
}
return;
}
if (pointsEqual(lastPoint, pt)) {
return;
}
segments.push({ a: lastPoint, b: pt });
if (currentPolygon) {
currentPolygon.push(pt);
}
lastPoint = pt;
};
for (let i = 0; i < vertices.length; i++) {
const vertex = vertices[i];
const command = vertex.command || (i === 0 ? Commands.move : Commands.line);
if (command === Commands.move) {
closePolygon(false);
const pt = createPoint(vertex.x, vertex.y);
currentPolygon = [pt];
firstPoint = pt;
lastPoint = pt;
prevVertex = vertex;
continue;
}
if (!prevVertex) {
prevVertex = vertices[Math.max(i - 1, 0)];
}
if (command === Commands.line) {
appendPoint(createPoint(vertex.x, vertex.y));
} else if (command === Commands.curve) {
const subdivisions = getSubdivisions(vertex, prevVertex, limit);
for (let j = 1; j < subdivisions.length; j++) {
const sv = subdivisions[j];
appendPoint(createPoint(sv.x, sv.y));
}
appendPoint(createPoint(vertex.x, vertex.y));
} else if (command === Commands.arc) {
const arcPoints = sampleArcPoints(prevVertex, vertex, limit);
for (let j = 0; j < arcPoints.length; j++) {
appendPoint(arcPoints[j]);
}
} else if (command === Commands.close) {
closePolygon(true);
prevVertex = vertex;
continue;
} else {
appendPoint(createPoint(vertex.x, vertex.y));
}
prevVertex = vertex;
}
if (currentPolygon) {
const shouldForceClose = !!path._closed || !!path.closed || firstPoint && lastPoint && !pointsEqual(firstPoint, lastPoint);
closePolygon(shouldForceClose);
}
return { polygons, segments };
}
function pointInPolygons(polygons, x, y) {
let inside = false;
for (let i = 0; i < polygons.length; i++) {
const polygon = polygons[i];
if (!polygon || polygon.length < 3) {
continue;
}
let lastIndex = polygon.length - 1;
for (let j = 0; j < polygon.length; j++) {
const v0 = polygon[lastIndex];
const v1 = polygon[j];
const intersects = v1.y > y !== v0.y > y && x < (v0.x - v1.x) * (y - v1.y) / (v0.y - v1.y || 1e-12) + v1.x;
if (intersects) {
inside = !inside;
}
lastIndex = j;
}
}
return inside;
}
function distanceToSegmentSquared(x, y, a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
const ddx2 = x - a.x;
const ddy2 = y - a.y;
return ddx2 * ddx2 + ddy2 * ddy2;
}
const t = ((x - a.x) * dx + (y - a.y) * dy) / (dx * dx + dy * dy);
const clamped = Math.max(0, Math.min(1, t));
const cx = a.x + clamped * dx;
const cy = a.y + clamped * dy;
const ddx = x - cx;
const ddy = y - cy;
return ddx * ddx + ddy * ddy;
}
function distanceToSegments(segments, x, y) {
if (!segments || segments.length === 0) {
return Infinity;
}
let minDistance = Infinity;
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const distance = distanceToSegmentSquared(x, y, segment.a, segment.b);
if (distance < minDistance) {
minDistance = distance;
}
}
return Math.sqrt(minDistance);
}
function hasVisibleFill(shape, override) {
if (typeof override === "boolean") {
return override;
}
const fill = shape.fill;
if (!fill && fill !== 0) {
return false;
}
if (typeof fill === "string") {
return !TRANSPARENT_REGEX.test(fill);
}
return true;
}
function hasVisibleStroke(shape, override) {
const linewidth = typeof shape.linewidth === "number" ? shape.linewidth : shape._linewidth || 0;
if (typeof override === "boolean") {
return override && linewidth > 0;
}
if (!(linewidth > 0)) {
return false;
}
const stroke = shape.stroke;
if (!stroke && stroke !== 0) {
return false;
}
if (typeof stroke === "string") {
return !TRANSPARENT_REGEX.test(stroke);
}
return true;
}
function boundsContains(rect, x, y, tolerance = 0) {
if (!rect) {
return false;
}
const left = rect.left - tolerance;
const right = rect.right + tolerance;
const top = rect.top - tolerance;
const bottom = rect.bottom + tolerance;
return x >= left && x <= right && y >= top && y <= bottom;
}
// src/utils/path.js
var EPSILON2 = Number.EPSILON;
function isRelativeAnchor(anchor2) {
return !(typeof anchor2.relative === "boolean") || !!anchor2.relative;
}
function setHandleComponent(anchor2, side, dx, dy) {
const controls = anchor2.controls;
if (!controls || !controls[side]) {
return;
}
if (Math.abs(dx) < EPSILON2 && Math.abs(dy) < EPSILON2) {
if (isRelativeAnchor(anchor2)) {
controls[side].clear();
} else {
controls[side].set(anchor2.x, anchor2.y);
}
return;
}
if (isRelativeAnchor(anchor2)) {
controls[side].set(dx, dy);
} else {
controls[side].set(anchor2.x + dx, anchor2.y + dy);
}
}
function clearHandleComponent(anchor2, side) {
setHandleComponent(anchor2, side, 0, 0);
}
function getHandleOffset(anchor2, side) {
const controls = anchor2.controls;
if (!controls || !controls[side]) {
return { x: 0, y: 0 };
}
if (isRelativeAnchor(anchor2)) {
return { x: controls[side].x, y: controls[side].y };
}
return {
x: controls[side].x - anchor2.x,
y: controls[side].y - anchor2.y
};
}
function hasNonZeroHandle(anchor2, side) {
const offset = getHandleOffset(anchor2, side);
return Math.abs(offset.x) > EPSILON2 || Math.abs(offset.y) > EPSILON2;
}
function updateAnchorCommand(anchor2) {
if (anchor2.command === Commands.move || anchor2.command === Commands.close) {
return;
}
anchor2.command = hasNonZeroHandle(anchor2, "left") || hasNonZeroHandle(anchor2, "right") ? Commands.curve : Commands.line;
}
function inheritRelative(anchor2, reference) {
if (typeof reference.relative === "boolean") {
anchor2.relative = reference.relative;
}
}
function isSegmentCurved(a, b) {
return hasNonZeroHandle(b, "right") || hasNonZeroHandle(a, "left") || hasNonZeroHandle(a, "right") || hasNonZeroHandle(b, "left") || a.command === Commands.curve || b.command === Commands.curve;
}
function lerpPoint(a, b, t) {
return {
x: lerp(a.x, b.x, t),
y: lerp(a.y, b.y, t)
};
}
function getAbsoluteHandle(anchor2, side) {
const controls = anchor2.controls && anchor2.controls[side];
if (!controls) {
return { x: anchor2.x, y: anchor2.y };
}
if (isRelativeAnchor(anchor2)) {
return { x: anchor2.x + controls.x, y: anchor2.y + controls.y };
}
return { x: controls.x, y: controls.y };
}
function splitSubdivisionSegment(start, end, t) {
const right = start.controls && start.controls.right;
const left = end.controls && end.controls.left;
const p0 = { x: start.x, y: start.y };
const p1 = right ? getAbsoluteHandle(start, "right") : __spreadValues({}, p0);
const p3 = { x: end.x, y: end.y };
const p2 = left ? getAbsoluteHandle(end, "left") : __spreadValues({}, p3);
const q0 = lerpPoint(p0, p1, t);
const q1 = lerpPoint(p1, p2, t);
const q2 = lerpPoint(p2, p3, t);
const r0 = lerpPoint(q0, q1, t);
const r1 = lerpPoint(q1, q2, t);
const point = lerpPoint(r0, r1, t);
const anchor2 = new Anchor(point.x, point.y);
inheritRelative(anchor2, start);
setHandleComponent(anchor2, "left", r0.x - point.x, r0.y - point.y);
setHandleComponent(anchor2, "right", r1.x - point.x, r1.y - point.y);
anchor2.command = Commands.curve;
return {
anchor: anchor2,
startOut: q0,
endIn: q2
};
}
function applyGlobalSmooth(vertices, from, to, closed2, loop2, asymmetric) {
const length = vertices.length;
const amount = to - from + 1;
let n = amount - 1;
let padding = loop2 ? Math.min(amount, 4) : 1;
let paddingLeft = padding;
let paddingRight = padding;
if (!closed2) {
paddingLeft = Math.min(1, from);
paddingRight = Math.min(1, length - to - 1);
}
n += paddingLeft + paddingRight;
if (n <= 1) {
return;
}
const knots = new Array(n + 1);
for (let i = 0, j = from - paddingLeft; i <= n; i += 1, j += 1) {
const index = mod(j, length);
knots[i] = vertices[index];
}
let x = knots[0].x + 2 * knots[1].x;
let y = knots[0].y + 2 * knots[1].y;
let f = 2;
const n1 = n - 1;
const rx = [x];
const ry = [y];
const rf = [f];
const px = new Array(n + 1);
const py = new Array(n + 1);
for (let i = 1; i < n; i += 1) {
const internal = i < n1;
const a = internal ? 1 : asymmetric ? 1 : 2;
const b = internal ? 4 : asymmetric ? 2 : 7;
const u = internal ? 4 : asymmetric ? 3 : 8;
const v = internal ? 2 : asymmetric ? 0 : 1;
const m = a / f;
f = rf[i] = b - m;
x = rx[i] = u * knots[i].x + v * knots[i + 1].x - m * x;
y = ry[i] = u * knots[i].y + v * knots[i + 1].y - m * y;
}
px[n1] = rx[n1] / rf[n1];
py[n1] = ry[n1] / rf[n1];
for (let i = n - 2; i >= 0; i -= 1) {
px[i] = (rx[i] - px[i + 1]) / rf[i];
py[i] = (ry[i] - py[i + 1]) / rf[i];
}
px[n] = (3 * knots[n].x - px[n1]) / 2;
py[n] = (3 * knots[n].y - py[n1]) / 2;
const max5 = n - paddingRight;
for (let i = paddingLeft, j = from; i <= max5; i += 1, j += 1) {
const index = mod(j, length);
const anchor2 = vertices[index];
const hx = px[i] - anchor2.x;
const hy = py[i] - anchor2.y;
if (loop2 || i < max5) {
setHandleComponent(anchor2, "right", hx, hy);
} else {
clearHandleComponent(anchor2, "right");
}
if (loop2 || i > paddingLeft) {
setHandleComponent(anchor2, "left", -hx, -hy);
} else {
clearHandleComponent(anchor2, "left");
}
updateAnchorCommand(anchor2);
}
}
function applyCatmullRom(anchor2, prev, next, factor, clampIn, clampOut) {
const p0 = prev || anchor2;
const p1 = anchor2;
const p2 = next || anchor2;
const d1 = Vector.distanceBetween(p0, p1);
const d2 = Vector.distanceBetween(p1, p2);
const a = factor === void 0 ? 0.5 : factor;
const d1a = Math.pow(d1, a);
const d2a = Math.pow(d2, a);
const d1_2a = d1a * d1a;
const d2_2a = d2a * d2a;
if (!clampIn && prev) {
const A = 2 * d2_2a + 3 * d2a * d1a + d1_2a;
const N = 3 * d2a * (d2a + d1a);
if (N !== 0) {
const hx = (d2_2a * p0.x + A * p1.x - d1_2a * p2.x) / N - p1.x;
const hy = (d2_2a * p0.y + A * p1.y - d1_2a * p2.y) / N - p1.y;
setHandleComponent(anchor2, "left", hx, hy);
} else {
clearHandleComponent(anchor2, "left");
}
} else {
clearHandleComponent(anchor2, "left");
}
if (!clampOut && next) {
const A = 2 * d1_2a + 3 * d1a * d2a + d2_2a;
const N = 3 * d1a * (d1a + d2a);
if (N !== 0) {
const hx = (d1_2a * p2.x + A * p1.x - d2_2a * p0.x) / N - p1.x;
const hy = (d1_2a * p2.y + A * p1.y - d2_2a * p0.y) / N - p1.y;
setHandleComponent(anchor2, "right", hx, hy);
} else {
clearHandleComponent(anchor2, "right");
}
} else {
clearHandleComponent(anchor2, "right");
}
updateAnchorCommand(anchor2);
}
function applyGeometric(anchor2, prev, next, factor, clampIn, clampOut) {
if (!(prev && next)) {
if (!prev) {
clearHandleComponent(anchor2, "left");
}
if (!next) {
clearHandleComponent(anchor2, "right");
}
updateAnchorCommand(anchor2);
return;
}
const p0 = prev;
const p1 = anchor2;
const p2 = next;
const d1 = Vector.distanceBetween(p0, p1);
const d2 = Vector.distanceBetween(p1, p2);
const total = d1 + d2;
const tension = factor === void 0 ? 0.4 : factor;
const vector3 = { x: p0.x - p2.x, y: p0.y - p2.y };
if (!clampIn && total !== 0) {
const k = tension * d1 / total;
setHandleComponent(anchor2, "left", vector3.x * k, vector3.y * k);
} else {
clearHandleComponent(anchor2, "left");
}
if (!clampOut && total !== 0) {
const k = tension * d1 / total - tension;
setHandleComponent(anchor2, "right", vector3.x * k, vector3.y * k);
} else {
clearHandleComponent(anchor2, "right");
}
updateAnchorCommand(anchor2);
}
function applyLocalSmooth(vertices, from, to, closed2, loop2, options) {
const type = options.type || "catmull-rom";
const factor = options.factor;
const length = vertices.length;
for (let i = from; i <= to; i += 1) {
const index = mod(i, length);
const anchor2 = vertices[index];
if (anchor2.command === Commands.move) {
clearHandleComponent(anchor2, "left");
clearHandleComponent(anchor2, "right");
continue;
}
const prevIndex = i === from && !loop2 ? null : i - 1;
const nextIndex = i === to && !loop2 ? null : i + 1;
const prev = prevIndex === null ? null : vertices[mod(prevIndex, length)];
const next = nextIndex === null ? null : vertices[mod(nextIndex, length)];
const clampIn = prevIndex === null;
const clampOut = nextIndex === null;
if (type === "geometric") {
applyGeometric(anchor2, prev, next, factor, clampIn, clampOut);
} else {
applyCatmullRom(anchor2, prev, next, factor, clampIn, clampOut);
}
}
}
// src/path.js
var min = Math.min;
var max = Math.max;
var ceil = Math.ceil;
var floor2 = Math.floor;
var vector = new Vector();
var hitTestMatrix = new Matrix2();
var _Path = class _Path extends Shape {
constructor(vertices, closed2, curved, manual) {
super();
/**
* @name Two.Path#_flagVertices
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#vertices} need updating.
*/
__publicField(this, "_flagVertices", true);
/**
* @name Two.Path#_flagLength
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#length} needs updating.
*/
__publicField(this, "_flagLength", true);
/**
* @name Two.Path#_flagFill
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#fill} needs updating.
*/
__publicField(this, "_flagFill", true);
/**
* @name Two.Path#_flagStroke
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#stroke} needs updating.
*/
__publicField(this, "_flagStroke", true);
/**
* @name Two.Path#_flagLinewidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#linewidth} needs updating.
*/
__publicField(this, "_flagLinewidth", true);
/**
* @name Two.Path#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#opacity} needs updating.
*/
__publicField(this, "_flagOpacity", true);
/**
* @name Two.Path#_flagVisible
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#visible} needs updating.
*/
__publicField(this, "_flagVisible", true);
/**
* @name Two.Path#_flagCap
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#cap} needs updating.
*/
__publicField(this, "_flagCap", true);
/**
* @name Two.Path#_flagJoin
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#join} needs updating.
*/
__publicField(this, "_flagJoin", true);
/**
* @name Two.Path#_flagMiter
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#miter} needs updating.
*/
__publicField(this, "_flagMiter", true);
/**
* @name Two.Path#_flagStrokeAttenuation
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#strokeAttenuation} needs updating.
*/
__publicField(this, "_flagStrokeAttenuation", true);
/**
* @name Two.Path#_flagMask
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#mask} needs updating.
*/
__publicField(this, "_flagMask", false);
/**
* @name Two.Path#_flagClip
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#clip} needs updating.
*/
__publicField(this, "_flagClip", false);
// Underlying Properties
/**
* @name Two.Path#_length
* @private
* @see {@link Two.Path#length}
*/
__publicField(this, "_length", 0);
/**
* @name Two.Path#_fill
* @private
* @see {@link Two.Path#fill}
*/
__publicField(this, "_fill", "#fff");
/**
* @name Two.Path#_stroke
* @private
* @see {@link Two.Path#stroke}
*/
__publicField(this, "_stroke", "#000");
/**
* @name Two.Path#_linewidth
* @private
* @see {@link Two.Path#linewidth}
*/
__publicField(this, "_linewidth", 1);
/**
* @name Two.Path#_opacity
* @private
* @see {@link Two.Path#opacity}
*/
__publicField(this, "_opacity", 1);
/**
* @name Two.Path#_visible
* @private
* @see {@link Two.Path#visible}
*/
__publicField(this, "_visible", true);
/**
* @name Two.Path#_cap
* @private
* @see {@link Two.Path#cap}
*/
__publicField(this, "_cap", "round");
/**
* @name Two.Path#_join
* @private
* @see {@link Two.Path#join}
*/
__publicField(this, "_join", "round");
/**
* @name Two.Path#_miter
* @private
* @see {@link Two.Path#miter}
*/
__publicField(this, "_miter", 4);
/**
* @name Two.Path#_closed
* @private
* @see {@link Two.Path#closed}
*/
__publicField(this, "_closed", true);
/**
* @name Two.Path#_curved
* @private
* @see {@link Two.Path#curved}
*/
__publicField(this, "_curved", false);
/**
* @name Two.Path#_automatic
* @private
* @see {@link Two.Path#automatic}
*/
__publicField(this, "_automatic", true);
/**
* @name Two.Path#_beginning
* @private
* @see {@link Two.Path#beginning}
*/
__publicField(this, "_beginning", 0);
/**
* @name Two.Path#_ending
* @private
* @see {@link Two.Path#ending}
*/
__publicField(this, "_ending", 1);
/**
* @name Two.Path#_mask
* @private
* @see {@link Two.Path#mask}
*/
__publicField(this, "_mask", null);
/**
* @name Two.Path#_clip
* @private
* @see {@link Two.Path#clip}
*/
__publicField(this, "_clip", false);
/**
* @name Two.Path#_dashes
* @private
* @see {@link Two.Path#dashes}
*/
__publicField(this, "_dashes", null);
/**
* @name Two.Path#_strokeAttenuation
* @private
* @see {@link Two.Path#strokeAttenuation}
*/
__publicField(this, "_strokeAttenuation", true);
for (let prop in proto10) {
Object.defineProperty(this, prop, proto10[prop]);
}
this._renderer.type = "path";
this._renderer.flagVertices = FlagVertices.bind(this);
this._renderer.bindVertices = BindVertices.bind(this);
this._renderer.unbindVertices = UnbindVertices.bind(this);
this._renderer.flagFill = FlagFill.bind(this);
this._renderer.flagStroke = FlagStroke.bind(this);
this._renderer.vertices = [];
this._renderer.collection = [];
this.closed = !!closed2;
this.curved = !!curved;
this.beginning = 0;
this.ending = 1;
this.fill = "#fff";
this.stroke = "#000";
this.linewidth = 1;
this.opacity = 1;
this.className = "";
this.visible = true;
this.cap = "butt";
this.join = "miter";
this.miter = 4;
this.vertices = vertices;
this.automatic = !manual;
this.dashes = [];
this.dashes.offset = 0;
}
/**
* @name Two.Path.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Path} to create a new instance
* @returns {Two.Path}
* @description Create a new {@link Two.Path} from an object notation of a {@link Two.Path}.
* @nota-bene Works in conjunction with {@link Two.Path#toObject}
*/
static fromObject(obj) {
const fill = typeof obj.fill === "string" ? obj.fill : getEffectFromObject(obj.fill);
const stroke = typeof obj.stroke === "string" ? obj.stroke : getEffectFromObject(obj.stroke);
const path = new _Path().copy(__spreadProps(__spreadValues({}, obj), { fill, stroke }));
if ("id" in obj) {
path.id = obj.id;
}
return path;
}
/**
* @name Two.Path#copy
* @function
* @param {Two.Path} path - The reference {@link Two.Path}
* @description Copy the properties of one {@link Two.Path} onto another.
*/
copy(path) {
super.copy.call(this, path);
if (path.vertices) {
this.vertices = [];
for (let j = 0; j < path.vertices.length; j++) {
const v = path.vertices[j];
if (v instanceof Anchor) {
this.vertices.push(path.vertices[j].clone());
} else {
this.vertices.push(new Anchor().copy(v));
}
}
}
for (let i = 0; i < _Path.Properties.length; i++) {
const k = _Path.Properties[i];
if (k in path) {
this[k] = path[k];
}
}
return this;
}
/**
* @name Two.Path#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Path}
* @description Create a new instance of {@link Two.Path} with the same properties of the current path.
*/
clone(parent) {
const clone = new _Path();
for (let j = 0; j < this.vertices.length; j++) {
clone.vertices.push(this.vertices[j].clone());
}
for (let i = 0; i < _Path.Properties.length; i++) {
const k = _Path.Properties[i];
clone[k] = this[k];
}
clone.className = this.className;
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
if (parent) {
parent.add(clone);
}
return clone._update();
}
/**
* @name Two.Path#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
* @nota-bene Works in conjunction with {@link Two.Path.fromObject}
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = "path";
result.vertices = this.vertices.map((v) => v.toObject());
_.each(
_Path.Properties,
(k) => {
if (typeof this[k] !== "undefined") {
if (this[k].toObject) {
result[k] = this[k].toObject();
} else {
result[k] = this[k];
}
}
},
this
);
return result;
}
/**
* @name Two.Path#dispose
* @function
* @returns {Two.Path}
* @description Release the path's renderer resources and detach all events.
* This method cleans up vertices collection events, individual vertex events,
* control point events, and disposes fill/stroke effects (calling dispose()
* on Gradients and Textures for thorough cleanup) while preserving the
* renderer type for potential re-attachment to a new renderer.
*/
dispose() {
super.dispose();
if (this.vertices && typeof this.vertices.unbind === "function") {
try {
this.vertices.unbind();
} catch (e) {
}
}
if (this.vertices) {
for (let i = 0; i < this.vertices.length; i++) {
const v = this.vertices[i];
if (typeof v.unbind === "function") {
v.unbind();
}
if (v.controls) {
if (v.controls.left && typeof v.controls.left.unbind === "function") {
v.controls.left.unbind();
}
if (v.controls.right && typeof v.controls.right.unbind === "function") {
v.controls.right.unbind();
}
}
}
}
if (typeof this.fill === "object" && typeof this.fill.dispose === "function") {
this.fill.dispose();
} else if (typeof this.fill === "object" && typeof this.fill.unbind === "function") {
this.fill.unbind();
}
if (typeof this.stroke === "object" && typeof this.stroke.dispose === "function") {
this.stroke.dispose();
} else if (typeof this.stroke === "object" && typeof this.stroke.unbind === "function") {
this.stroke.unbind();
}
return this;
}
/**
* @name Two.Path#noFill
* @function
* @description Short hand method to set fill to `none`.
*/
noFill() {
this.fill = "none";
return this;
}
/**
* @name Two.Path#noStroke
* @function
* @description Short hand method to set stroke to `none`.
*/
noStroke() {
this.stroke = "none";
this.linewidth = 0;
return this;
}
/**
* @name Two.Path#corner
* @function
* @description Orient the vertices of the shape to the upper left-hand corner of the path.
*/
corner() {
const rect = this.getBoundingClientRect(true);
const hw = rect.width / 2;
const hh = rect.height / 2;
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
for (let i = 0; i < this.vertices.length; i++) {
const v = this.vertices[i];
v.x -= cx;
v.y -= cy;
v.x += hw;
v.y += hh;
}
if (this.mask) {
this.mask.translation.x -= cx;
this.mask.translation.x += hw;
this.mask.translation.y -= cy;
this.mask.translation.y += hh;
}
return this;
}
/**
* @name Two.Path#center
* @function
* @description Orient the vertices of the shape to the center of the path.
*/
center() {
const rect = this.getBoundingClientRect(true);
const cx = rect.left + rect.width / 2 - this.translation.x;
const cy = rect.top + rect.height / 2 - this.translation.y;
for (let i = 0; i < this.vertices.length; i++) {
const v = this.vertices[i];
v.x -= cx;
v.y -= cy;
}
if (this.mask) {
this.mask.translation.x -= cx;
this.mask.translation.y -= cy;
}
return this;
}
/**
* @name Two.Path#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the path.
*/
getBoundingClientRect(shallow) {
let matrix, border, l, i, v0, v1;
let left = Infinity, right = -Infinity, top = Infinity, bottom = -Infinity;
this._update(true);
matrix = shallow ? this.matrix : this.worldMatrix;
border = (this.linewidth || 0) / 2;
l = this._renderer.vertices.length;
if (this.linewidth > 0 || this.stroke && !/(transparent|none)/i.test(this.stroke)) {
if (this.matrix.manual) {
const { scaleX, scaleY } = decomposeMatrix(
matrix.elements[0],
matrix.elements[3],
matrix.elements[1],
matrix.elements[4],
matrix.elements[2],
matrix.elements[5]
);
if (typeof scaleX === "number" && typeof scaleY === "number") {
border = Math.max(scaleX, scaleY) * (this.linewidth || 0) / 2;
}
} else {
border *= typeof this.scale === "number" ? this.scale : Math.max(this.scale.x, this.scale.y);
}
}
if (l <= 0) {
return {
width: 0,
height: 0
};
}
for (i = 0; i < l; i++) {
v1 = this._renderer.vertices[i];
v0 = this._renderer.vertices[(i + l - 1) % l];
const [v0x, v0y] = matrix.multiply(v0.x, v0.y);
const [v1x, v1y] = matrix.multiply(v1.x, v1.y);
if (v0.controls && v1.controls) {
let rx = v0.controls.right.x;
let ry = v0.controls.right.y;
if (v0.relative) {
rx += v0.x;
ry += v0.y;
}
let [c0x, c0y] = matrix.multiply(rx, ry);
let lx = v1.controls.left.x;
let ly = v1.controls.left.y;
if (v1.relative) {
lx += v1.x;
ly += v1.y;
}
let [c1x, c1y] = matrix.multiply(lx, ly);
const bb = getCurveBoundingBox(v0x, v0y, c0x, c0y, c1x, c1y, v1x, v1y);
top = min(bb.min.y - border, top);
left = min(bb.min.x - border, left);
right = max(bb.max.x + border, right);
bottom = max(bb.max.y + border, bottom);
} else {
if (i <= 1) {
top = min(v0y - border, top);
left = min(v0x - border, left);
right = max(v0x + border, right);
bottom = max(v0y + border, bottom);
}
top = min(v1y - border, top);
left = min(v1x - border, left);
right = max(v1x + border, right);
bottom = max(v1y + border, bottom);
}
}
return {
top,
left,
right,
bottom,
width: right - left,
height: bottom - top
};
}
/**
* @name Two.Path#contains
* @function
* @param {Number} x - x coordinate to hit test against
* @param {Number} y - y coordinate to hit test against
* @param {Object} [options] - Optional options object
* @param {Boolean} [options.ignoreVisibility] - If `true`, hit test against `path.visible = false` shapes
* @param {Number} [options.tolerance] - Padding to hit test against in pixels
* @returns {Boolean}
* @description Check to see if coordinates are within a {@link Two.Path}'s bounding rectangle
* @nota-bene Expects *world-space coordinates* – the same pixel-space you get from the renderer (e.g., mouse `clientX`/`clientY` adjusted for the canvas’s offset and pixel ratio).
*/
contains(x, y, options) {
const opts = options || {};
const ignoreVisibility = opts.ignoreVisibility === true;
if (!ignoreVisibility && this.visible === false) {
return false;
}
if (!ignoreVisibility && typeof this.opacity === "number" && this.opacity <= 0) {
return false;
}
const tolerance = typeof opts.tolerance === "number" ? opts.tolerance : 0;
this._update(true);
const rect = this.getBoundingClientRect();
if (!rect || x < rect.left - tolerance || x > rect.right + tolerance || y < rect.top - tolerance || y > rect.bottom + tolerance) {
return false;
}
const matrix = this.worldMatrix;
const inverse = matrix && matrix.inverse(hitTestMatrix);
if (!inverse) {
return super.contains(x, y, opts);
}
const [localX, localY] = inverse.multiply(x, y, 1);
const precision = typeof opts.precision === "number" && !Number.isNaN(opts.precision) ? Math.max(1, Math.floor(opts.precision)) : 8;
const fillTest = hasVisibleFill(this, opts.fill);
const strokeTest = hasVisibleStroke(this, opts.stroke);
const { polygons, segments } = buildPathHitParts(this, precision);
if (fillTest && polygons.length > 0) {
if (pointInPolygons(polygons, localX, localY)) {
return true;
}
}
if (strokeTest && segments.length > 0) {
const linewidth = typeof this.linewidth === "number" ? this.linewidth : 0;
if (linewidth > 0) {
const distance = distanceToSegments(segments, localX, localY);
if (distance <= linewidth / 2 + tolerance) {
return true;
}
}
}
if (!fillTest && !strokeTest) {
return super.contains(x, y, opts);
}
if (fillTest && polygons.length === 0) {
return super.contains(x, y, opts);
}
return false;
}
/**
* @name Two.Path#getPointAt
* @function
* @param {Number} t - Percentage value describing where on the {@link Two.Path} to estimate and assign coordinate values.
* @param {Two.Vector} [obj] - Object to apply calculated x, y to. If none available returns new `Object`.
* @returns {Object}
* @description Given a float `t` from 0 to 1, return a point or assign a passed `obj`'s coordinates to that percentage on this {@link Two.Path}'s curve.
*/
getPointAt(t, obj) {
let ia, ib, result;
let x, x1, x2, x3, x4, y, y1, y2, y3, y4, left, right;
let target = this.length * Math.min(Math.max(t, 0), 1);
const length = this.vertices.length;
const last = length - 1;
let a = null;
let b = null;
for (let i = 0, l = this._lengths.length, sum = 0; i < l; i++) {
if (sum + this._lengths[i] >= target) {
if (this._closed) {
ia = mod(i, length);
ib = mod(i - 1, length);
if (i === 0) {
ia = ib;
ib = i;
}
} else {
ia = i;
ib = Math.min(Math.max(i - 1, 0), last);
}
a = this.vertices[ia];
b = this.vertices[ib];
target -= sum;
if (this._lengths[i] !== 0) {
t = target / this._lengths[i];
} else {
t = 0;
}
break;
}
sum += this._lengths[i];
}
if (a === null || b === null) {
return null;
}
if (!a) {
return b;
} else if (!b) {
return a;
}
right = b.controls && b.controls.right;
left = a.controls && a.controls.left;
x1 = b.x;
y1 = b.y;
x2 = (right || b).x;
y2 = (right || b).y;
x3 = (left || a).x;
y3 = (left || a).y;
x4 = a.x;
y4 = a.y;
if (right && b.relative) {
x2 += b.x;
y2 += b.y;
}
if (left && a.relative) {
x3 += a.x;
y3 += a.y;
}
x = getComponentOnCubicBezier(t, x1, x2, x3, x4);
y = getComponentOnCubicBezier(t, y1, y2, y3, y4);
const t1x = lerp(x1, x2, t);
const t1y = lerp(y1, y2, t);
const t2x = lerp(x2, x3, t);
const t2y = lerp(y2, y3, t);
const t3x = lerp(x3, x4, t);
const t3y = lerp(y3, y4, t);
const brx = lerp(t1x, t2x, t);
const bry = lerp(t1y, t2y, t);
const alx = lerp(t2x, t3x, t);
const aly = lerp(t2y, t3y, t);
if (_.isObject(obj)) {
obj.x = x;
obj.y = y;
if (obj instanceof Anchor) {
obj.controls.left.x = brx;
obj.controls.left.y = bry;
obj.controls.right.x = alx;
obj.controls.right.y = aly;
if (!(typeof obj.relative === "boolean") || obj.relative) {
obj.controls.left.x -= x;
obj.controls.left.y -= y;
obj.controls.right.x -= x;
obj.controls.right.y -= y;
}
}
obj.t = t;
return obj;
}
result = new Anchor(
x,
y,
brx - x,
bry - y,
alx - x,
aly - y,
this._curved ? Commands.curve : Commands.line
);
result.t = t;
return result;
}
/**
* @name Two.Path#plot
* @function
* @description Based on closed / curved and sorting of vertices plot where all points should be and where the respective handles should be too.
* @nota-bene While this method is public it is internally called by {@link Two.Path#_update} when `automatic = true`.
*/
plot() {
if (this.curved) {
getCurveFromPoints(this._collection, this.closed);
return this;
}
for (let i = 0; i < this._collection.length; i++) {
this._collection[i].command = i === 0 ? Commands.move : Commands.line;
}
return this;
}
/**
* @name Two.Path#smooth
* @function
* @param {Object} [options] - Configuration for smoothing.
* @param {String} [options.type='continuous'] - Type of smoothing algorithm.
* @param {Number} [options.from=0] - Index of vertices to start smoothing
* @param {Number} [options.to=1] - Index of vertices to terminate smoothing
* @description Adjust vertex handles to generate smooth curves without toggling `automatic`.
*/
smooth(options) {
const opts = options || {};
const type = opts.type || "continuous";
const vertices = this._collection;
const length = vertices.length;
if (length < 2) {
return this;
}
const closed2 = this._closed || length > 0 && vertices[length - 1] && vertices[length - 1].command === Commands.close;
const resolveIndex = (value, defaultIndex) => {
if (value === void 0 || value === null) {
return defaultIndex;
}
if (typeof value === "number") {
if (closed2) {
return mod(value, length);
}
let index = value;
if (index < 0) {
index += length;
}
return Math.min(Math.max(index, 0), length - 1);
}
const idx = vertices.indexOf(value);
return idx !== -1 ? idx : defaultIndex;
};
const loop2 = closed2 && opts.from === void 0 && opts.to === void 0;
let from = resolveIndex(opts.from, 0);
let to = resolveIndex(opts.to, length - 1);
if (from > to) {
if (closed2) {
from -= length;
} else {
const temp2 = from;
from = to;
to = temp2;
}
}
const rangeLength = to - from + 1;
for (let i = 0; i < rangeLength; i += 1) {
const index = mod(from + i, length);
const anchor2 = vertices[index];
const isOpenStart = !closed2 && index === 0;
if (anchor2.command === Commands.move && !isOpenStart) {
anchor2.command = Commands.line;
}
}
if (type === "continuous" || type === "asymmetric") {
applyGlobalSmooth(
vertices,
from,
to,
closed2,
loop2,
type === "asymmetric"
);
} else if (type === "catmull-rom" || type === "geometric") {
const range = {
type,
factor: opts.factor
};
applyLocalSmooth(vertices, from, to, closed2, loop2, range);
} else {
throw new Error(
`Path.smooth does not support type "${type}". Try 'continuous', 'asymmetric', 'catmull-rom', or 'geometric'.`
);
}
this._automatic = false;
this._flagVertices = true;
this._flagLength = true;
return this;
}
/**
* @name Two.Path#subdivide
* @function
* @param {Number} limit - How many times to recurse subdivisions.
* @description Insert a {@link Two.Anchor} at the midpoint between every item in {@link Two.Path#vertices}.
*/
subdivide(limit) {
this._update();
const vertices = this.vertices;
const length = vertices.length;
if (length < 2) {
return this;
}
const points = [];
let prevOriginal = null;
let subpathStartOriginal = null;
for (let i = 0; i < length; i += 1) {
const currentOriginal = vertices[i];
if (!prevOriginal || currentOriginal.command === Commands.move) {
const clone = currentOriginal.clone();
points.push(clone);
prevOriginal = currentOriginal;
subpathStartOriginal = currentOriginal;
continue;
}
const isCurve = isSegmentCurved(currentOriginal, prevOriginal);
if (isCurve) {
const subdivided = getSubdivisions(
currentOriginal,
prevOriginal,
limit
);
const steps = subdivided.length;
const prevClone = points[points.length - 1];
let startSegment = prevClone.clone();
let endSegment = currentOriginal.clone();
let prevCloneRef = prevClone;
let prevT = 0;
if (steps <= 1) {
const currentClone = currentOriginal.clone();
points.push(currentClone);
} else {
for (let j = 1; j < steps; j += 1) {
const globalT = j / steps;
const denom = 1 - prevT;
const localT = denom <= Number.EPSILON ? globalT : (globalT - prevT) / denom;
const split = splitSubdivisionSegment(
startSegment,
endSegment,
localT
);
setHandleComponent(
prevCloneRef,
"right",
split.startOut.x - prevCloneRef.x,
split.startOut.y - prevCloneRef.y
);
const newAnchor = split.anchor;
points.push(newAnchor);
prevCloneRef = newAnchor;
startSegment = newAnchor.clone();
prevT = globalT;
setHandleComponent(
endSegment,
"left",
split.endIn.x - endSegment.x,
split.endIn.y - endSegment.y
);
}
const currentClone = currentOriginal.clone();
currentClone.controls.left.copy(endSegment.controls.left);
points.push(currentClone);
}
} else {
const subdivided = getSubdivisions(
currentOriginal,
prevOriginal,
limit
);
for (let j = 1; j < subdivided.length; j += 1) {
const anchor2 = subdivided[j];
inheritRelative(anchor2, prevOriginal);
clearHandleComponent(anchor2, "left");
clearHandleComponent(anchor2, "right");
anchor2.command = Commands.line;
points.push(anchor2);
}
const currentClone = currentOriginal.clone();
points.push(currentClone);
}
prevOriginal = currentOriginal;
if (currentOriginal.command === Commands.close) {
prevOriginal = subpathStartOriginal;
}
}
this._automatic = false;
this._curved = false;
this.vertices = points;
return this;
}
/**
* @name Two.Path#_updateLength
* @function
* @private
* @param {Number} [limit] -
* @param {Boolean} [silent=false] - If set to `true` then the path isn't updated before calculation. Useful for internal use.
* @description Recalculate the {@link Two.Path#length} value.
*/
_updateLength(limit, silent) {
if (!silent) {
this._update();
}
const length = this.vertices.length;
const last = length - 1;
const closed2 = false;
let b = this.vertices[last];
let sum = 0;
if (typeof this._lengths === "undefined") {
this._lengths = [];
}
_.each(
this.vertices,
function(a, i) {
if (i <= 0 && !closed2 || a.command === Commands.move) {
b = a;
this._lengths[i] = 0;
return;
}
this._lengths[i] = getCurveLength2(a, b, limit);
sum += this._lengths[i];
b = a;
},
this
);
this._length = sum;
this._flagLength = false;
return this;
}
/**
* @name Two.Path#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices) {
if (this._automatic) {
this.plot();
}
if (this._flagLength) {
this._updateLength(void 0, true);
}
const l = this._collection.length;
const closed2 = this._closed;
const beginning = Math.min(this._beginning, this._ending);
const ending = Math.max(this._beginning, this._ending);
const bid = getIdByLength(this, beginning * this._length);
const eid = getIdByLength(this, ending * this._length);
const low = ceil(bid);
const high = floor2(eid);
let left, right, prev, next, v, i;
this._renderer.vertices.length = 0;
for (i = 0; i < l; i++) {
if (this._renderer.collection.length <= i) {
this._renderer.collection.push(new Anchor());
}
if (i > high && !right) {
v = this._renderer.collection[i].copy(this._collection[i]);
this.getPointAt(ending, v);
v.command = this._renderer.collection[i].command;
this._renderer.vertices.push(v);
right = v;
prev = this._collection[i - 1];
if (prev && prev.controls) {
if (v.relative) {
v.controls.right.clear();
} else {
v.controls.right.copy(v);
}
if (prev.relative) {
this._renderer.collection[i - 1].controls.right.copy(prev.controls.right).lerp(Vector.zero, 1 - v.t);
} else {
this._renderer.collection[i - 1].controls.right.copy(prev.controls.right).lerp(prev, 1 - v.t);
}
}
} else if (i >= low && i <= high) {
v = this._renderer.collection[i].copy(this._collection[i]);
this._renderer.vertices.push(v);
if (i === high && contains(this, ending)) {
right = v;
if (!closed2 && right.controls) {
if (right.relative) {
right.controls.right.clear();
} else {
right.controls.right.copy(right);
}
}
} else if (i === low && contains(this, beginning)) {
left = v;
left.command = Commands.move;
if (!closed2 && left.controls) {
if (left.relative) {
left.controls.left.clear();
} else {
left.controls.left.copy(left);
}
}
}
}
}
if (low > 0 && !left) {
i = low - 1;
v = this._renderer.collection[i].copy(this._collection[i]);
this.getPointAt(beginning, v);
v.command = Commands.move;
this._renderer.vertices.unshift(v);
next = this._collection[i + 1];
if (next && next.controls) {
v.controls.left.clear();
if (next.relative) {
this._renderer.collection[i + 1].controls.left.copy(next.controls.left).lerp(Vector.zero, v.t);
} else {
vector.copy(next);
this._renderer.collection[i + 1].controls.left.copy(next.controls.left).lerp(next, v.t);
}
}
}
}
Shape.prototype._update.apply(this, arguments);
return this;
}
/**
* @name Two.Path#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagVertices = this._flagLength = this._flagFill = this._flagStroke = this._flagLinewidth = this._flagOpacity = this._flagVisible = this._flagCap = this._flagJoin = this._flagMiter = this._flagClip = this._flagStrokeAttenuation = false;
Shape.prototype.flagReset.call(this);
return this;
}
};
/**
* @name Two.Path.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Path}.
*/
__publicField(_Path, "Properties", [
"fill",
"stroke",
"linewidth",
"opacity",
"visible",
"cap",
"join",
"miter",
"closed",
"curved",
"automatic",
"beginning",
"ending",
"dashes",
"strokeAttenuation"
]);
__publicField(_Path, "Utils", {
getCurveLength: getCurveLength2
});
var Path = _Path;
var proto10 = {
linewidth: {
enumerable: true,
get: function() {
return this._linewidth;
},
set: function(v) {
this._linewidth = v;
this._flagLinewidth = true;
}
},
opacity: {
enumerable: true,
get: function() {
return this._opacity;
},
set: function(v) {
this._opacity = v;
this._flagOpacity = true;
}
},
visible: {
enumerable: true,
get: function() {
return this._visible;
},
set: function(v) {
this._visible = v;
this._flagVisible = true;
}
},
cap: {
enumerable: true,
get: function() {
return this._cap;
},
set: function(v) {
this._cap = v;
this._flagCap = true;
}
},
join: {
enumerable: true,
get: function() {
return this._join;
},
set: function(v) {
this._join = v;
this._flagJoin = true;
}
},
miter: {
enumerable: true,
get: function() {
return this._miter;
},
set: function(v) {
this._miter = v;
this._flagMiter = true;
}
},
fill: {
enumerable: true,
get: function() {
return this._fill;
},
set: function(f) {
if (this._fill instanceof Gradient || this._fill instanceof LinearGradient || this._fill instanceof RadialGradient || this._fill instanceof Texture) {
this._fill.unbind(Events.Types.change, this._renderer.flagFill);
}
this._fill = f;
this._flagFill = true;
if (this._fill instanceof Gradient || this._fill instanceof LinearGradient || this._fill instanceof RadialGradient || this._fill instanceof Texture) {
this._fill.bind(Events.Types.change, this._renderer.flagFill);
}
}
},
stroke: {
enumerable: true,
get: function() {
return this._stroke;
},
set: function(f) {
if (this._stroke instanceof Gradient || this._stroke instanceof LinearGradient || this._stroke instanceof RadialGradient || this._stroke instanceof Texture) {
this._stroke.unbind(Events.Types.change, this._renderer.flagStroke);
}
this._stroke = f;
this._flagStroke = true;
if (this._stroke instanceof Gradient || this._stroke instanceof LinearGradient || this._stroke instanceof RadialGradient || this._stroke instanceof Texture) {
this._stroke.bind(Events.Types.change, this._renderer.flagStroke);
}
}
},
/**
* @name Two.Path#length
* @property {Number} - The sum of distances between all {@link Two.Path#vertices}.
*/
length: {
get: function() {
if (this._flagLength) {
this._updateLength();
}
return this._length;
}
},
closed: {
enumerable: true,
get: function() {
return this._closed;
},
set: function(v) {
this._closed = !!v;
this._flagVertices = true;
}
},
curved: {
enumerable: true,
get: function() {
return this._curved;
},
set: function(v) {
this._curved = !!v;
this._flagVertices = true;
}
},
automatic: {
enumerable: true,
get: function() {
return this._automatic;
},
set: function(v) {
if (v === this._automatic) {
return;
}
this._automatic = !!v;
const method = this._automatic ? "ignore" : "listen";
_.each(this.vertices, function(v2) {
v2[method]();
});
}
},
beginning: {
enumerable: true,
get: function() {
return this._beginning;
},
set: function(v) {
this._beginning = v;
this._flagVertices = true;
}
},
ending: {
enumerable: true,
get: function() {
return this._ending;
},
set: function(v) {
this._ending = v;
this._flagVertices = true;
}
},
vertices: {
enumerable: true,
get: function() {
return this._collection;
},
set: function(vertices) {
const bindVertices = this._renderer.bindVertices;
const unbindVertices = this._renderer.unbindVertices;
if (this._collection) {
this._collection.unbind(Events.Types.insert, bindVertices).unbind(Events.Types.remove, unbindVertices);
}
if (vertices instanceof Collection) {
this._collection = vertices;
} else {
this._collection = new Collection(vertices || []);
}
this._collection.bind(Events.Types.insert, bindVertices).bind(Events.Types.remove, unbindVertices);
bindVertices(this._collection);
}
},
/**
* @name Two.Path#mask
* @property {Two.Shape} - The shape whose alpha property becomes a clipping area for the path.
* @nota-bene This property is currently not working because of SVG spec issues found here {@link https://code.google.com/p/chromium/issues/detail?id=370951}.
*/
mask: {
enumerable: true,
get: function() {
return this._mask;
},
set: function(v) {
this._mask = v;
this._flagMask = true;
if (_.isObject(v) && !v.clip) {
v.clip = true;
}
}
},
/**
* @name Two.Path#clip
* @property {Boolean} - Tells Two.js renderer if this object represents a mask for another object (or not).
*/
clip: {
enumerable: true,
get: function() {
return this._clip;
},
set: function(v) {
this._clip = v;
this._flagClip = true;
}
},
dashes: {
enumerable: true,
get: function() {
return this._dashes;
},
set: function(v) {
if (typeof v.offset !== "number") {
v.offset = this.dashes && this._dashes.offset || 0;
}
this._dashes = v;
}
},
/**
* @name Two.Path#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
* @description When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
*/
strokeAttenuation: {
enumerable: true,
get: function() {
return this._strokeAttenuation;
},
set: function(v) {
this._strokeAttenuation = !!v;
this._flagStrokeAttenuation = true;
this._flagLinewidth = true;
}
}
};
function FlagVertices() {
this._flagVertices = true;
this._flagLength = true;
if (this.parent) {
this.parent._flagLength = true;
}
}
function BindVertices(items) {
let i = items.length;
while (i--) {
items[i].bind(Events.Types.change, this._renderer.flagVertices);
}
this._renderer.flagVertices();
}
function UnbindVertices(items) {
let i = items.length;
while (i--) {
items[i].unbind(Events.Types.change, this._renderer.flagVertices);
}
this._renderer.flagVertices();
}
function FlagFill() {
this._flagFill = true;
}
function FlagStroke() {
this._flagStroke = true;
}
// src/shapes/rectangle.js
var _Rectangle = class _Rectangle extends Path {
constructor(x, y, width, height) {
const points = [
new Anchor(),
new Anchor(),
new Anchor(),
new Anchor()
// new Anchor() // TODO: Figure out how to handle this for `beginning` / `ending` animations
];
super(points, true, false, true);
/**
* @name Two.Rectangle#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Rectangle#width} needs updating.
*/
__publicField(this, "_flagWidth", false);
/**
* @name Two.Rectangle#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Rectangle#height} needs updating.
*/
__publicField(this, "_flagHeight", false);
/**
* @name Two.Rectangle#_width
* @private
* @see {@link Two.Rectangle#width}
*/
__publicField(this, "_width", 0);
/**
* @name Two.Rectangle#_height
* @private
* @see {@link Two.Rectangle#height}
*/
__publicField(this, "_height", 0);
__publicField(this, "_origin", null);
this._renderer.type = "rectangle";
for (let prop in proto11) {
Object.defineProperty(this, prop, proto11[prop]);
}
this.width = typeof width === "number" ? width : 1;
this.height = typeof height === "number" ? height : 1;
this.origin = new Vector();
if (typeof x === "number") {
this.translation.x = x;
}
if (typeof y === "number") {
this.translation.y = y;
}
this._update();
}
/**
* @name Two.Rectangle.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Rectangle} to create a new instance
* @returns {Two.Rectangle}
* @description Create a new {@link Two.Rectangle} from an object notation of a {@link Two.Rectangle}.
* @nota-bene Works in conjunction with {@link Two.Rectangle#toObject}
*/
static fromObject(obj) {
const rectangle = new _Rectangle().copy(obj);
if ("id" in obj) {
rectangle.id = obj.id;
}
return rectangle;
}
/**
* @name Two.Rectangle#copy
* @function
* @param {Two.Rectangle} rectangle - The reference {@link Two.Rectangle}
* @description Copy the properties of one {@link Two.Rectangle} onto another.
*/
copy(rectangle) {
super.copy.call(this, rectangle);
for (let i = 0; i < _Rectangle.Properties.length; i++) {
const k = _Rectangle.Properties[i];
if (k in rectangle) {
if (typeof rectangle[k] === "number") {
this[k] = rectangle[k];
} else if (this[k] instanceof Vector) {
this[k].copy(rectangle[k]);
}
}
}
return this;
}
/**
* @name Two.Rectangle#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagWidth || this._flagHeight) {
const xr = this._width / 2;
const yr = this._height / 2;
if (!this._closed && this.vertices.length === 4) {
this.vertices.push(new Anchor());
}
this.vertices[0].set(-xr, -yr).sub(this._origin).command = Commands.move;
this.vertices[1].set(xr, -yr).sub(this._origin).command = Commands.line;
this.vertices[2].set(xr, yr).sub(this._origin).command = Commands.line;
this.vertices[3].set(-xr, yr).sub(this._origin).command = Commands.line;
if (this.vertices[4]) {
this.vertices[4].set(-xr, -yr).sub(this._origin).command = Commands.line;
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Rectangle#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagWidth = this._flagHeight = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Rectangle#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Rectangle}
* @description Create a new instance of {@link Two.Rectangle} with the same properties of the current path.
*/
clone(parent) {
const clone = new _Rectangle(0, 0, this.width, this.height);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
if (clone[k] instanceof Vector) {
clone[k].copy(this[k]);
} else {
clone[k] = this[k];
}
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Rectangle#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "rectangle";
object.width = this.width;
object.height = this.height;
object.origin = this.origin.toObject();
return object;
}
};
/**
* @name Two.Rectangle.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Rectangle}.
*/
__publicField(_Rectangle, "Properties", ["width", "height", "origin"]);
var Rectangle = _Rectangle;
var proto11 = {
width: {
enumerable: true,
get: function() {
return this._width;
},
set: function(v) {
this._width = v;
this._flagWidth = true;
}
},
height: {
enumerable: true,
get: function() {
return this._height;
},
set: function(v) {
this._height = v;
this._flagHeight = true;
}
},
origin: {
enumerable: true,
get: function() {
return this._origin;
},
set: function(v) {
if (this._origin) {
this._origin.unbind(Events.Types.change, this._renderer.flagVertices);
}
this._origin = v;
this._origin.bind(Events.Types.change, this._renderer.flagVertices);
this._renderer.flagVertices();
}
}
};
// src/effects/sprite.js
var _Sprite = class _Sprite extends Rectangle {
constructor(src, ox, oy, cols, rows, frameRate) {
super(ox, oy, 0, 0);
/**
* @name Two.Sprite#_flagTexture
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#texture} needs updating.
*/
__publicField(this, "_flagTexture", false);
/**
* @name Two.Sprite#_flagColumns
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#columns} need updating.
*/
__publicField(this, "_flagColumns", false);
/**
* @name Two.Sprite#_flagRows
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#rows} need updating.
*/
__publicField(this, "_flagRows", false);
/**
* @name Two.Sprite#_flagFrameRate
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#flagFrameRate} needs updating.
*/
__publicField(this, "_flagFrameRate", false);
/**
* @name Two.Sprite#_flagIndex
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#index} needs updating.
*/
__publicField(this, "_flagIndex", false);
// Private variables
/**
* @name Two.Sprite#_amount
* @private
* @property {Number} - Number of frames for a given {@link Two.Sprite}.
*/
__publicField(this, "_amount", 1);
/**
* @name Two.Sprite#_duration
* @private
* @property {Number} - Number of milliseconds a {@link Two.Sprite}.
*/
__publicField(this, "_duration", 0);
/**
* @name Two.Sprite#_startTime
* @private
* @property {Milliseconds} - Epoch time in milliseconds of when the {@link Two.Sprite} started.
*/
__publicField(this, "_startTime", 0);
/**
* @name Two.Sprite#_playing
* @private
* @property {Boolean} - Dictates whether the {@link Two.Sprite} is animating or not.
*/
__publicField(this, "_playing", false);
/**
* @name Two.Sprite#_firstFrame
* @private
* @property {Number} - The frame the {@link Two.Sprite} should start with.
*/
__publicField(this, "_firstFrame", 0);
/**
* @name Two.Sprite#_lastFrame
* @private
* @property {Number} - The frame the {@link Two.Sprite} should end with.
*/
__publicField(this, "_lastFrame", 0);
/**
* @name Two.Sprite#_loop
* @private
* @property {Boolean} - Dictates whether the {@link Two.Sprite} should loop or not.
*/
__publicField(this, "_loop", true);
// Exposed through getter-setter
/**
* @name Two.Sprite#_texture
* @private
* @see {@link Two.Sprite#texture}
*/
__publicField(this, "_texture", null);
/**
* @name Two.Sprite#_columns
* @private
* @see {@link Two.Sprite#columns}
*/
__publicField(this, "_columns", 1);
/**
* @name Two.Sprite#_rows
* @private
* @see {@link Two.Sprite#rows}
*/
__publicField(this, "_rows", 1);
/**
* @name Two.Sprite#_frameRate
* @private
* @see {@link Two.Sprite#frameRate}
*/
__publicField(this, "_frameRate", 0);
/**
* @name Two.Sprite#_index
* @private
* @property {Number} - The current frame the {@link Two.Sprite} is currently displaying.
*/
__publicField(this, "_index", 0);
/**
* @name Two.Sprite#_origin
* @private
* @see {@link Two.Sprite#origin}
*/
__publicField(this, "_origin", null);
this._renderer.type = "sprite";
for (let prop in proto12) {
Object.defineProperty(this, prop, proto12[prop]);
}
this.noStroke();
this.noFill();
if (src instanceof Texture) {
this.texture = src;
} else if (typeof src === "string") {
this.texture = new Texture(src);
}
this.origin = new Vector();
this._update();
if (typeof cols === "number") {
this.columns = cols;
}
if (typeof rows === "number") {
this.rows = rows;
}
if (typeof frameRate === "number") {
this.frameRate = frameRate;
}
this.index = 0;
}
/**
* @name Two.Sprite.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Sprite} to create a new instance
* @returns {Two.Sprite}
* @description Create a new {@link Two.Sprite} from an object notation of a {@link Two.Sprite}.
* @nota-bene Works in conjunction with {@link Two.Sprite#toObject}
*/
static fromObject(obj) {
const sprite = new _Sprite().copy(obj);
if ("id" in obj) {
sprite.id = obj.id;
}
return sprite;
}
/**
* @name Two.Sprite#copy
* @function
* @param {Two.Sprite} sprite - The reference {@link Two.Sprite}
* @description Copy the properties of one {@link Two.Sprite} onto another.
*/
copy(sprite) {
super.copy.call(this, sprite);
for (let i = 0; i < _Sprite.Properties.length; i++) {
const k = _Sprite.Properties[i];
if (k in sprite) {
this[k] = sprite[k];
}
}
return this;
}
/**
* @name Two.Sprite#play
* @function
* @param {Number} [firstFrame=0] - The index of the frame to start the animation with.
* @param {Number} [lastFrame] - The index of the frame to end the animation with. Defaults to the last item in the {@link Two.Sprite#textures}.
* @param {Function} [onLastFrame] - Optional callback function to be triggered after playing the last frame. This fires multiple times when the sprite is looped.
* @description Initiate animation playback of a {@link Two.Sprite}.
*/
play(firstFrame, lastFrame, onLastFrame) {
this._playing = true;
this._firstFrame = 0;
this._lastFrame = this.amount - 1;
this._startTime = _.performance.now();
if (typeof firstFrame === "number") {
this._firstFrame = firstFrame;
}
if (typeof lastFrame === "number") {
this._lastFrame = lastFrame;
}
if (typeof onLastFrame === "function") {
this._onLastFrame = onLastFrame;
} else {
delete this._onLastFrame;
}
if (this._index !== this._firstFrame) {
this._startTime -= 1e3 * Math.abs(this._index - this._firstFrame) / this._frameRate;
}
return this;
}
/**
* @name Two.Sprite#pause
* @function
* @description Halt animation playback of a {@link Two.Sprite}.
*/
pause() {
this._playing = false;
return this;
}
/**
* @name Two.Sprite#stop
* @function
* @description Halt animation playback of a {@link Two.Sprite} and set the current frame back to the first frame.
*/
stop() {
this._playing = false;
this._index = 0;
return this;
}
/**
* @name Two.Sprite#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Sprite}
* @description Create a new instance of {@link Two.Sprite} with the same properties of the current sprite.
*/
clone(parent) {
const clone = new _Sprite(
this.texture,
this.translation.x,
this.translation.y,
this.columns,
this.rows,
this.frameRate
);
if (this.playing) {
clone.play(this._firstFrame, this._lastFrame);
}
clone.loop = this.loop;
clone.firstFrame = this.firstFrame;
clone.lastFrame = this.lastFrame;
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Sprite#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "sprite";
object.texture = this.texture.toObject();
object.columns = this.columns;
object.rows = this.rows;
object.frameRate = this.frameRate;
object.index = this.index;
object.firstFrame = this.firstFrame;
object.lastFrame = this.lastFrame;
object.loop = this.loop;
return object;
}
/**
* @name Two.Sprite#dispose
* @function
* @returns {Two.Sprite}
* @description Release the sprite's renderer resources and detach all events.
* This method stops any running animation, clears animation callbacks, disposes
* the texture (calling dispose() for thorough cleanup), and inherits comprehensive
* cleanup from the Rectangle/Path hierarchy while preserving the renderer type
* for potential re-attachment.
*/
dispose() {
super.dispose();
if (this._playing) {
this._playing = false;
}
this._onLastFrame = null;
this._startTime = 0;
if (this._texture && typeof this._texture.dispose === "function") {
this._texture.dispose();
} else if (this._texture && typeof this._texture.unbind === "function") {
this._texture.unbind();
}
return this;
}
/**
* @name Two.Sprite#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
const effect = this._texture;
const cols = this._columns;
const rows = this._rows;
let width, height, elapsed, amount, duration;
let index, iw, ih, frames;
if (effect) {
if (this._flagColumns || this._flagRows) {
this._amount = this._columns * this._rows;
}
if (this._flagFrameRate) {
this._duration = 1e3 * this._amount / this._frameRate;
}
if (this._flagTexture) {
this.fill = effect;
}
if (effect.loaded) {
iw = effect.image.width;
ih = effect.image.height;
width = iw / cols;
height = ih / rows;
amount = this._amount;
if (this.width !== width) {
this.width = width;
}
if (this.height !== height) {
this.height = height;
}
if (this._playing && this._frameRate > 0) {
if (_.isNaN(this._lastFrame)) {
this._lastFrame = amount - 1;
}
elapsed = _.performance.now() - this._startTime;
frames = this._lastFrame + 1;
duration = 1e3 * (frames - this._firstFrame) / this._frameRate;
if (this._loop) {
elapsed = elapsed % duration;
} else {
elapsed = Math.min(elapsed, duration);
}
index = lerp(this._firstFrame, frames, elapsed / duration);
index = Math.floor(index);
if (index !== this._index) {
this._index = index;
if (index >= this._lastFrame - 1 && this._onLastFrame) {
this._onLastFrame();
}
}
}
const col = this._index % cols;
const row = Math.floor(this._index / cols);
const ox = -width * col + (iw - width) / 2;
const oy = -height * row + (ih - height) / 2;
if (ox !== effect.offset.x) {
effect.offset.x = ox;
}
if (oy !== effect.offset.y) {
effect.offset.y = oy;
}
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Sprite#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagTexture = this._flagColumns = this._flagRows = this._flagFrameRate = false;
super.flagReset.call(this);
return this;
}
};
/**
* @name Two.Sprite.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Sprite}.
*/
__publicField(_Sprite, "Properties", [
"texture",
"columns",
"rows",
"frameRate",
"index",
"firstFrame",
"lastFrame",
"loop"
]);
var Sprite = _Sprite;
var proto12 = {
texture: {
enumerable: true,
get: function() {
return this._texture;
},
set: function(v) {
this._texture = v;
this._flagTexture = true;
}
},
columns: {
enumerable: true,
get: function() {
return this._columns;
},
set: function(v) {
this._columns = v;
this._flagColumns = true;
}
},
rows: {
enumerable: true,
get: function() {
return this._rows;
},
set: function(v) {
this._rows = v;
this._flagRows = true;
}
},
frameRate: {
enumerable: true,
get: function() {
return this._frameRate;
},
set: function(v) {
this._frameRate = v;
this._flagFrameRate = true;
}
},
index: {
enumerable: true,
get: function() {
return this._index;
},
set: function(v) {
this._index = v;
this._flagIndex = true;
}
},
firstFrame: {
enumerable: true,
get: function() {
return this._firstFrame;
},
set: function(v) {
this._firstFrame = v;
}
},
lastFrame: {
enumerable: true,
get: function() {
return this._lastFrame;
},
set: function(v) {
this._lastFrame = v;
}
},
loop: {
enumerable: true,
get: function() {
return this._loop;
},
set: function(v) {
this._loop = !!v;
}
}
};
// src/children.js
var _ids;
var Children = class extends Collection {
constructor(children) {
children = Array.isArray(children) ? children : Array.prototype.slice.call(arguments);
super(children);
/**
* @name Two.Group.Children#ids
* @property {Object} - Map of all elements in the list keyed by `id`s.
*/
// N.B: Technique to disable enumeration on object
__privateAdd(this, _ids, {});
this.attach(children);
this.on(Events.Types.insert, this.attach);
this.on(Events.Types.remove, this.detach);
}
get ids() {
return __privateGet(this, _ids);
}
/**
* @function
* @name Two.Group.Children#attach
* @param {Two.Shape[]} children - The objects which extend {@link Two.Shape} to be added.
* @description Adds elements to the `ids` map.
*/
attach(children) {
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child && child.id) {
this.ids[child.id] = child;
}
}
return this;
}
/**
* @function
* @name Two.Group.Children#detach
* @param {Two.Shape[]} children - The objects which extend {@link Two.Shape} to be removed.
* @description Removes elements to the `ids` map.
*/
detach(children) {
for (let i = 0; i < children.length; i++) {
delete this.ids[children[i].id];
}
return this;
}
};
_ids = new WeakMap();
// src/shapes/arc-segment.js
var _ArcSegment = class _ArcSegment extends Path {
constructor(x, y, ir, or, sa, ea, res) {
const amount = res || Constants.Resolution * 3;
const points = [];
for (let i = 0; i < amount; i++) {
points.push(new Anchor());
}
super(points, true, false, true);
/**
* @name Two.ArcSegment#_flagStartAngle
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#startAngle} needs updating.
*/
__publicField(this, "_flagStartAngle", false);
/**
* @name Two.ArcSegment#_flagEndAngle
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#endAngle} needs updating.
*/
__publicField(this, "_flagEndAngle", false);
/**
* @name Two.ArcSegment#_flagInnerRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#innerRadius} needs updating.
*/
__publicField(this, "_flagInnerRadius", false);
/**
* @name Two.ArcSegment#_flagOuterRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#outerRadius} needs updating.
*/
__publicField(this, "_flagOuterRadius", false);
/**
* @name Two.ArcSegment#_startAngle
* @private
* @see {@link Two.ArcSegment#startAngle}
*/
__publicField(this, "_startAngle", 0);
/**
* @name Two.ArcSegment#_endAngle
* @private
* @see {@link Two.ArcSegment#endAngle}
*/
__publicField(this, "_endAngle", TWO_PI);
/**
* @name Two.ArcSegment#_innerRadius
* @private
* @see {@link Two.ArcSegment#innerRadius}
*/
__publicField(this, "_innerRadius", 0);
/**
* @name Two.ArcSegment#_outerRadius
* @private
* @see {@link Two.ArcSegment#outerRadius}
*/
__publicField(this, "_outerRadius", 0);
this._renderer.type = "arc-segment";
for (let prop in proto13) {
Object.defineProperty(this, prop, proto13[prop]);
}
if (typeof ir === "number") {
this.innerRadius = ir;
}
if (typeof or === "number") {
this.outerRadius = or;
}
if (typeof sa === "number") {
this.startAngle = sa;
}
if (typeof ea === "number") {
this.endAngle = ea;
}
this._update();
if (typeof x === "number") {
this.translation.x = x;
}
if (typeof y === "number") {
this.translation.y = y;
}
}
/**
* @name Two.ArcSegment.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.ArcSegment} to create a new instance
* @returns {Two.ArcSegment}
* @description Create a new {@link Two.ArcSegment} from an object notation of a {@link Two.ArcSegment}.
* @nota-bene Works in conjunction with {@link Two.ArcSegment#toObject}
*/
static fromObject(obj) {
const segment = new _ArcSegment().copy(obj);
if ("id" in obj) {
segment.id = obj.id;
}
return segment;
}
/**
* @name Two.ArcSegment#copy
* @function
* @param {Two.ArcSegment} arcSegment - The reference {@link Two.ArcSegment}
* @description Copy the properties of one {@link Two.ArcSegment} onto another.
*/
copy(arcSegment) {
super.copy.call(this, arcSegment);
for (let i = 0; i < _ArcSegment.Properties.length; i++) {
const k = _ArcSegment.Properties[i];
if (k in arcSegment && typeof arcSegment[k] === "number") {
this[k] = arcSegment[k];
}
}
return this;
}
/**
* @name Two.ArcSegment#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagStartAngle || this._flagEndAngle || this._flagInnerRadius || this._flagOuterRadius) {
const sa = this._startAngle;
const ea = this._endAngle;
const ir = this._innerRadius;
const or = this._outerRadius;
const connected = mod(sa, TWO_PI) === mod(ea, TWO_PI);
const punctured = ir > 0;
const vertices = this.vertices;
let length = punctured ? vertices.length / 2 : vertices.length;
let command, id = 0;
let i, last, pct, v, theta, step, x, y, amp;
if (connected) {
length--;
} else if (!punctured) {
length -= 2;
}
for (i = 0, last = length - 1; i < length; i++) {
pct = i / last;
v = vertices[id];
theta = pct * (ea - sa) + sa;
step = (ea - sa) / length;
x = or * Math.cos(theta);
y = or * Math.sin(theta);
switch (i) {
case 0:
command = Commands.move;
break;
default:
command = Commands.curve;
}
v.command = command;
v.x = x;
v.y = y;
v.controls.left.clear();
v.controls.right.clear();
if (v.command === Commands.curve) {
amp = or * step / Math.PI;
v.controls.left.x = amp * Math.cos(theta - HALF_PI);
v.controls.left.y = amp * Math.sin(theta - HALF_PI);
v.controls.right.x = amp * Math.cos(theta + HALF_PI);
v.controls.right.y = amp * Math.sin(theta + HALF_PI);
if (i === 1) {
v.controls.left.multiplyScalar(2);
}
if (i === last) {
v.controls.right.multiplyScalar(2);
}
}
id++;
}
if (punctured) {
if (connected) {
vertices[id].command = Commands.close;
id++;
} else {
length--;
last = length - 1;
}
for (i = 0; i < length; i++) {
pct = i / last;
v = vertices[id];
theta = (1 - pct) * (ea - sa) + sa;
step = (ea - sa) / length;
x = ir * Math.cos(theta);
y = ir * Math.sin(theta);
command = Commands.curve;
if (i <= 0) {
command = connected ? Commands.move : Commands.line;
}
v.command = command;
v.x = x;
v.y = y;
v.controls.left.clear();
v.controls.right.clear();
if (v.command === Commands.curve) {
amp = ir * step / Math.PI;
v.controls.left.x = amp * Math.cos(theta + HALF_PI);
v.controls.left.y = amp * Math.sin(theta + HALF_PI);
v.controls.right.x = amp * Math.cos(theta - HALF_PI);
v.controls.right.y = amp * Math.sin(theta - HALF_PI);
if (i === 1) {
v.controls.left.multiplyScalar(2);
}
if (i === last) {
v.controls.right.multiplyScalar(2);
}
}
id++;
}
vertices[id].copy(vertices[0]);
vertices[id].command = Commands.line;
} else if (!connected) {
vertices[id].command = Commands.line;
vertices[id].x = 0;
vertices[id].y = 0;
id++;
vertices[id].copy(vertices[0]);
vertices[id].command = Commands.line;
}
}
super._update.call(this);
return this;
}
/**
* @name Two.ArcSegment#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
super.flagReset.call(this);
this._flagStartAngle = this._flagEndAngle = this._flagInnerRadius = this._flagOuterRadius = false;
return this;
}
/**
* @name Two.ArcSegment#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.ArcSegment}
* @description Create a new instance of {@link Two.ArcSegment} with the same properties of the current path.
*/
clone(parent) {
const ir = this.innerRadius;
const or = this.outerRadius;
const sa = this.startAngle;
const ea = this.endAngle;
const resolution = this.vertices.length;
const clone = new _ArcSegment(0, 0, ir, or, sa, ea, resolution);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.ArcSegment#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "arc-segment";
for (let i = 0; i < _ArcSegment.Properties.length; i++) {
const k = _ArcSegment.Properties[i];
object[k] = this[k];
}
return object;
}
};
/**
* @name Two.ArcSegment.Properties
* @property {String[]} - A list of properties that are on every {@link Two.ArcSegment}.
*/
__publicField(_ArcSegment, "Properties", ["startAngle", "endAngle", "innerRadius", "outerRadius"]);
var ArcSegment = _ArcSegment;
var proto13 = {
startAngle: {
enumerable: true,
get: function() {
return this._startAngle;
},
set: function(v) {
this._startAngle = v;
this._flagStartAngle = true;
}
},
endAngle: {
enumerable: true,
get: function() {
return this._endAngle;
},
set: function(v) {
this._endAngle = v;
this._flagEndAngle = true;
}
},
innerRadius: {
enumerable: true,
get: function() {
return this._innerRadius;
},
set: function(v) {
this._innerRadius = v;
this._flagInnerRadius = true;
}
},
outerRadius: {
enumerable: true,
get: function() {
return this._outerRadius;
},
set: function(v) {
this._outerRadius = v;
this._flagOuterRadius = true;
}
}
};
// src/shapes/circle.js
var cos2 = Math.cos;
var sin2 = Math.sin;
var _Circle = class _Circle extends Path {
constructor(ox, oy, r, resolution) {
const amount = resolution ? Math.max(resolution, 2) : 4;
const points = [];
for (let i = 0; i < amount; i++) {
points.push(new Anchor(0, 0, 0, 0, 0, 0));
}
super(points, true, true, true);
/**
* @name Two.Circle#_flagRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.Circle#radius} needs updating.
*/
__publicField(this, "_flagRadius", false);
/**
* @name Two.Circle#_radius
* @private
* @see {@link Two.Circle#radius}
*/
__publicField(this, "_radius", 0);
this._renderer.type = "circle";
for (let prop in proto14) {
Object.defineProperty(this, prop, proto14[prop]);
}
if (typeof r === "number") {
this.radius = r;
}
this._update();
if (typeof ox === "number") {
this.translation.x = ox;
}
if (typeof oy === "number") {
this.translation.y = oy;
}
}
/**
* @name Two.Circle.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Circle} to create a new instance
* @returns {Two.Circle}
* @description Create a new {@link Two.Circle} from an object notation of a {@link Two.Circle}.
* @nota-bene Works in conjunction with {@link Two.Circle#toObject}
*/
static fromObject(obj) {
const circle = new _Circle().copy(obj);
if ("id" in obj) {
circle.id = obj.id;
}
return circle;
}
/**
* @name Two.Circle#copy
* @function
* @param {Two.Circle} circle - The reference {@link Two.Circle}
* @description Copy the properties of one {@link Two.Circle} onto another.
*/
copy(circle) {
super.copy.call(this, circle);
for (let i = 0; i < _Circle.Properties.length; i++) {
const k = _Circle.Properties[i];
if (k in circle && typeof circle[k] === "number") {
this[k] = circle[k];
}
}
return this;
}
/**
* @name Two.Circle#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagRadius) {
let length = this.vertices.length;
if (!this._closed && length > 2) {
length -= 1;
}
const c = 4 / 3 * Math.tan(Math.PI / (length * 2));
const radius = this._radius;
const rc = radius * c;
for (let i = 0; i < this.vertices.length; i++) {
const pct = i / length;
const theta = pct * TWO_PI;
const x = radius * cos2(theta);
const y = radius * sin2(theta);
const lx = rc * cos2(theta - HALF_PI);
const ly = rc * sin2(theta - HALF_PI);
const rx = rc * cos2(theta + HALF_PI);
const ry = rc * sin2(theta + HALF_PI);
const v = this.vertices[i];
v.command = i === 0 ? Commands.move : Commands.curve;
v.set(x, y);
v.controls.left.set(lx, ly);
v.controls.right.set(rx, ry);
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Circle#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagRadius = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Circle#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Circle}
* @description Create a new instance of {@link Two.Circle} with the same properties of the current path.
*/
clone(parent) {
const clone = new _Circle(0, 0, this.radius, this.vertices.length);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Circle#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "circle";
for (let i = 0; i < _Circle.Properties.length; i++) {
const k = _Circle.Properties[i];
object[k] = this[k];
}
return object;
}
};
/**
* @name Two.Circle.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Circle}.
*/
__publicField(_Circle, "Properties", ["radius"]);
var Circle = _Circle;
var proto14 = {
radius: {
enumerable: true,
get: function() {
return this._radius;
},
set: function(v) {
this._radius = v;
this._flagRadius = true;
}
}
};
// src/shapes/ellipse.js
var cos3 = Math.cos;
var sin3 = Math.sin;
var _Ellipse = class _Ellipse extends Path {
constructor(x, y, rx, ry, resolution) {
if (typeof ry !== "number" && typeof rx === "number") {
ry = rx;
}
const amount = resolution ? Math.max(resolution, 2) : 4;
const points = [];
for (let i = 0; i < amount; i++) {
points.push(new Anchor());
}
super(points, true, true, true);
/**
* @name Two.Ellipse#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Ellipse#width} needs updating.
*/
__publicField(this, "_flagWidth", false);
/**
* @name Two.Ellipse#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Ellipse#height} needs updating.
*/
__publicField(this, "_flagHeight", false);
/**
* @name Two.Ellipse#_width
* @private
* @see {@link Two.Ellipse#width}
*/
__publicField(this, "_width", 0);
/**
* @name Two.Ellipse#_height
* @private
* @see {@link Two.Ellipse#height}
*/
__publicField(this, "_height", 0);
this._renderer.type = "ellipse";
for (let prop in proto15) {
Object.defineProperty(this, prop, proto15[prop]);
}
if (typeof rx === "number") {
this.width = rx * 2;
}
if (typeof ry === "number") {
this.height = ry * 2;
}
this._update();
if (typeof x === "number") {
this.translation.x = x;
}
if (typeof y === "number") {
this.translation.y = y;
}
}
/**
* @name Two.Ellipse.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Ellipse} to create a new instance
* @returns {Two.Ellipse}
* @description Create a new {@link Two.Ellipse} from an object notation of a {@link Two.Ellipse}.
* @nota-bene Works in conjunction with {@link Two.Ellipse#toObject}
*/
static fromObject(obj) {
const ellipse = new _Ellipse().copy(obj);
if ("id" in obj) {
ellipse.id = obj.id;
}
return ellipse;
}
/**
* @name Two.Ellipse#copy
* @function
* @param {Two.Ellipse} ellipse - The reference {@link Two.Ellipse}
* @description Copy the properties of one {@link Two.Ellipse} onto another.
*/
copy(ellipse) {
super.copy.call(this, ellipse);
for (let i = 0; i < _Ellipse.Properties.length; i++) {
const k = _Ellipse.Properties[i];
if (k in ellipse && typeof ellipse[k] === "number") {
this[k] = ellipse[k];
}
}
return this;
}
/**
* @name Two.Ellipse#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagWidth || this._flagHeight) {
let length = this.vertices.length;
if (!this._closed && length > 2) {
length -= 1;
}
const c = 4 / 3 * Math.tan(Math.PI / (this.vertices.length * 2));
const radiusX = this._width / 2;
const radiusY = this._height / 2;
for (let i = 0; i < this.vertices.length; i++) {
const pct = i / length;
const theta = pct * TWO_PI;
const x = radiusX * cos3(theta);
const y = radiusY * sin3(theta);
const lx = radiusX * c * cos3(theta - HALF_PI);
const ly = radiusY * c * sin3(theta - HALF_PI);
const rx = radiusX * c * cos3(theta + HALF_PI);
const ry = radiusY * c * sin3(theta + HALF_PI);
const v = this.vertices[i];
v.command = i === 0 ? Commands.move : Commands.curve;
v.set(x, y);
v.controls.left.set(lx, ly);
v.controls.right.set(rx, ry);
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Ellipse#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagWidth = this._flagHeight = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Ellipse#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Ellipse}
* @description Create a new instance of {@link Two.Ellipse} with the same properties of the current path.
*/
clone(parent) {
const rx = this.width / 2;
const ry = this.height / 2;
const resolution = this.vertices.length;
const clone = new _Ellipse(0, 0, rx, ry, resolution);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Ellipse#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "ellipse";
for (let i = 0; i < _Ellipse.Properties.length; i++) {
const k = _Ellipse.Properties[i];
object[k] = this[k];
}
return object;
}
};
/**
* @name Two.Ellipse.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Ellipse}.
*/
__publicField(_Ellipse, "Properties", ["width", "height"]);
var Ellipse = _Ellipse;
var proto15 = {
width: {
enumerable: true,
get: function() {
return this._width;
},
set: function(v) {
this._width = v;
this._flagWidth = true;
}
},
height: {
enumerable: true,
get: function() {
return this._height;
},
set: function(v) {
this._height = v;
this._flagHeight = true;
}
}
};
// src/shapes/points.js
var ceil2 = Math.ceil;
var floor3 = Math.floor;
var _Points = class _Points extends Shape {
constructor(vertices) {
super();
__publicField(this, "_flagVertices", true);
__publicField(this, "_flagLength", true);
__publicField(this, "_flagFill", true);
__publicField(this, "_flagStroke", true);
__publicField(this, "_flagLinewidth", true);
__publicField(this, "_flagOpacity", true);
__publicField(this, "_flagVisible", true);
__publicField(this, "_flagSize", true);
__publicField(this, "_flagSizeAttenuation", true);
__publicField(this, "_flagStrokeAttenuation", true);
__publicField(this, "_length", 0);
__publicField(this, "_fill", "#fff");
__publicField(this, "_stroke", "#000");
__publicField(this, "_linewidth", 1);
__publicField(this, "_opacity", 1);
__publicField(this, "_visible", true);
__publicField(this, "_size", 1);
__publicField(this, "_sizeAttenuation", false);
__publicField(this, "_beginning", 0);
__publicField(this, "_ending", 1);
__publicField(this, "_dashes", null);
__publicField(this, "_strokeAttenuation", true);
/**
* @name Two.Points#noFill
* @function
* @description Short hand method to set fill to `none`.
*/
__publicField(this, "noFill", Path.prototype.noFill);
/**
* @name Two.Points#noStroke
* @function
* @description Short hand method to set stroke to `none`.
*/
__publicField(this, "noStroke", Path.prototype.noStroke);
/**
* @name Two.Points#corner
* @function
* @description Orient the vertices of the shape to the upper left-hand corner of the points object.
*/
__publicField(this, "corner", Path.prototype.corner);
/**
* @name Two.Points#center
* @function
* @description Orient the vertices of the shape to the center of the points object.
*/
__publicField(this, "center", Path.prototype.center);
/**
* @name Two.Points#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the path.
*/
__publicField(this, "getBoundingClientRect", Path.prototype.getBoundingClientRect);
/**
* @name Two.Points#_updateLength
* @function
* @private
* @param {Number} [limit] -
* @param {Boolean} [silent=false] - If set to `true` then the points object isn't updated before calculation. Useful for internal use.
* @description Recalculate the {@link Two.Points#length} value.
*/
__publicField(this, "_updateLength", Path.prototype._updateLength);
for (let prop in proto16) {
Object.defineProperty(this, prop, proto16[prop]);
}
this._renderer.type = "points";
this._renderer.flagVertices = FlagVertices.bind(this);
this._renderer.bindVertices = BindVertices.bind(this);
this._renderer.unbindVertices = UnbindVertices.bind(this);
this._renderer.flagFill = FlagFill.bind(this);
this._renderer.flagStroke = FlagStroke.bind(this);
this._renderer.vertices = null;
this._renderer.collection = null;
this.size = 1;
this.sizeAttenuation = false;
this.beginning = 0;
this.ending = 1;
this.fill = "#fff";
this.stroke = "#000";
this.linewidth = 1;
this.opacity = 1;
this.className = "";
this.visible = true;
this.vertices = vertices;
this.dashes = [];
this.dashes.offset = 0;
}
/**
* @name Two.Points.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Points} to create a new instance
* @returns {Two.Points}
* @description Create a new {@link Two.Points} from an object notation of a {@link Two.Points}.
* @nota-bene Works in conjunction with {@link Two.Points#toObject}
*/
static fromObject(obj) {
const fill = typeof obj.fill === "string" ? obj.fill : getEffectFromObject(obj.fill);
const stroke = typeof obj.stroke === "string" ? obj.stroke : getEffectFromObject(obj.stroke);
const points = new _Points().copy(__spreadProps(__spreadValues({}, obj), { fill, stroke }));
if ("id" in obj) {
points.id = obj.id;
}
return points;
}
/**
* @name Two.Points#copy
* @function
* @param {Two.Points} points - The reference {@link Two.Points}
* @description Copy the properties of one {@link Two.Points} onto another.
*/
copy(points) {
super.copy.call(this, points);
for (let j = 0; j < points.vertices.length; j++) {
const v = points.vertices[j];
if (v instanceof Anchor) {
this.vertices.push(points.vertices[j].clone());
} else {
this.vertices.push(new Anchor().copy(v));
}
}
for (let i = 0; i < _Points.Properties.length; i++) {
const k = _Points.Properties[i];
if (k in points) {
this[k] = points[k];
}
}
return this;
}
/**
* @name Two.Points#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Points}
* @description Create a new instance of {@link Two.Points} with the same properties of the current path.
*/
clone(parent) {
const clone = new _Points();
for (let j = 0; j < this.vertices.length; j++) {
clone.vertices.push(this.vertices[j].clone());
}
for (let i = 0; i < _Points.Properties.length; i++) {
const k = _Points.Properties[i];
clone[k] = this[k];
}
clone.className = this.className;
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
if (parent) {
parent.add(clone);
}
return clone._update();
}
/**
* @name Two.Points#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the points object.
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = "points";
result.vertices = this.vertices.map((v) => v.toObject());
_.each(
_Points.Properties,
function(k) {
if (typeof this[k] !== "undefined") {
if (this[k].toObject) {
result[k] = this[k].toObject();
} else {
result[k] = this[k];
}
}
},
this
);
return result;
}
/**
* @name Two.Points#dispose
* @function
* @returns {Two.Points}
* @description Release the points' renderer resources and detach all events.
* This method cleans up vertices collection events, individual vertex events,
* and disposes fill/stroke effects (calling dispose() on Gradients and
* Textures for thorough cleanup) while preserving the renderer type for
* potential re-attachment to a new renderer.
*/
dispose() {
super.dispose();
if (this.vertices && typeof this.vertices.unbind === "function") {
try {
this.vertices.unbind();
} catch (e) {
}
}
if (this.vertices) {
for (let i = 0; i < this.vertices.length; i++) {
const v = this.vertices[i];
if (typeof v.unbind === "function") {
v.unbind();
}
}
}
if (typeof this.fill === "object" && typeof this.fill.dispose === "function") {
this.fill.dispose();
} else if (typeof this.fill === "object" && typeof this.fill.unbind === "function") {
this.fill.unbind();
}
if (typeof this.stroke === "object" && typeof this.stroke.dispose === "function") {
this.stroke.dispose();
} else if (typeof this.stroke === "object" && typeof this.stroke.unbind === "function") {
this.stroke.unbind();
}
return this;
}
/**
* @name Two.Points#subdivide
* @function
* @param {Number} limit - How many times to recurse subdivisions.
* @description Insert a {@link Two.Vector} at the midpoint between every item in {@link Two.Points#vertices}.
*/
subdivide(limit) {
this._update();
let points = [];
for (let i = 0; i < this.vertices.length; i++) {
const a = this.vertices[i];
const b = this.vertices[i - 1];
if (!b) {
continue;
}
const x1 = a.x;
const y1 = a.y;
const x2 = b.x;
const y2 = b.y;
const subdivisions = subdivide(x1, y1, x1, y1, x2, y2, x2, y2, limit);
points = points.concat(subdivisions);
}
this.vertices = points;
return this;
}
/**
* @name Two.Points#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices) {
if (this._flagLength) {
this._updateLength(void 0, true);
}
const beginning = Math.min(this._beginning, this._ending);
const ending = Math.max(this._beginning, this._ending);
const bid = getIdByLength(this, beginning * this._length);
const eid = getIdByLength(this, ending * this._length);
const low = ceil2(bid);
const high = floor3(eid);
let j = 0, v;
this._renderer.vertices = [];
this._renderer.collection = [];
for (let i = 0; i < this._collection.length; i++) {
if (i >= low && i <= high) {
v = this._collection[i];
this._renderer.collection.push(v);
this._renderer.vertices[j * 2 + 0] = v.x;
this._renderer.vertices[j * 2 + 1] = v.y;
j++;
}
}
}
super._update.apply(this, arguments);
return this;
}
/**
* @name Two.Points#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagVertices = this._flagLength = this._flagFill = this._flagStroke = this._flagLinewidth = this._flagOpacity = this._flagVisible = this._flagSize = this._flagSizeAttenuation = false;
super.flagReset.call(this);
return this;
}
};
/**
* @name Two.Points.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Points}.
*/
__publicField(_Points, "Properties", [
"fill",
"stroke",
"linewidth",
"opacity",
"visible",
"size",
"sizeAttenuation",
"beginning",
"ending",
"dashes",
"strokeAttenuation"
]);
var Points = _Points;
var proto16 = {
linewidth: {
enumerable: true,
get: function() {
return this._linewidth;
},
set: function(v) {
this._linewidth = v;
this._flagLinewidth = true;
}
},
opacity: {
enumerable: true,
get: function() {
return this._opacity;
},
set: function(v) {
this._opacity = v;
this._flagOpacity = true;
}
},
visible: {
enumerable: true,
get: function() {
return this._visible;
},
set: function(v) {
this._visible = v;
this._flagVisible = true;
}
},
size: {
enumerable: true,
get: function() {
return this._size;
},
set: function(v) {
this._size = v;
this._flagSize = true;
}
},
sizeAttenuation: {
enumerable: true,
get: function() {
return this._sizeAttenuation;
},
set: function(v) {
this._sizeAttenuation = v;
this._flagSizeAttenuation = true;
}
},
fill: {
enumerable: true,
get: function() {
return this._fill;
},
set: function(f) {
if (this._fill instanceof Gradient || this._fill instanceof LinearGradient || this._fill instanceof RadialGradient || this._fill instanceof Texture) {
this._fill.unbind(Events.Types.change, this._renderer.flagFill);
}
this._fill = f;
this._flagFill = true;
if (this._fill instanceof Gradient || this._fill instanceof LinearGradient || this._fill instanceof RadialGradient || this._fill instanceof Texture) {
this._fill.bind(Events.Types.change, this._renderer.flagFill);
}
}
},
stroke: {
enumerable: true,
get: function() {
return this._stroke;
},
set: function(f) {
if (this._stroke instanceof Gradient || this._stroke instanceof LinearGradient || this._stroke instanceof RadialGradient || this._stroke instanceof Texture) {
this._stroke.unbind(Events.Types.change, this._renderer.flagStroke);
}
this._stroke = f;
this._flagStroke = true;
if (this._stroke instanceof Gradient || this._stroke instanceof LinearGradient || this._stroke instanceof RadialGradient || this._stroke instanceof Texture) {
this._stroke.bind(Events.Types.change, this._renderer.flagStroke);
}
}
},
/**
* @name Two.Points#length
* @property {Number} - The sum of distances between all {@link Two.Points#vertices}.
*/
length: {
get: function() {
if (this._flagLength) {
this._updateLength();
}
return this._length;
}
},
beginning: {
enumerable: true,
get: function() {
return this._beginning;
},
set: function(v) {
this._beginning = v;
this._flagVertices = true;
}
},
ending: {
enumerable: true,
get: function() {
return this._ending;
},
set: function(v) {
this._ending = v;
this._flagVertices = true;
}
},
vertices: {
enumerable: true,
get: function() {
return this._collection;
},
set: function(vertices) {
const bindVertices = this._renderer.bindVertices;
const unbindVertices = this._renderer.unbindVertices;
if (this._collection) {
this._collection.unbind(Events.Types.insert, bindVertices).unbind(Events.Types.remove, unbindVertices);
}
if (vertices instanceof Collection) {
this._collection = vertices;
} else {
this._collection = new Collection(vertices || []);
}
this._collection.bind(Events.Types.insert, bindVertices).bind(Events.Types.remove, unbindVertices);
bindVertices(this._collection);
}
},
dashes: {
enumerable: true,
get: function() {
return this._dashes;
},
set: function(v) {
if (typeof v.offset !== "number") {
v.offset = this.dashes && this._dashes.offset || 0;
}
this._dashes = v;
}
},
/**
* @name Two.Points#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
* @description When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
*/
strokeAttenuation: {
enumerable: true,
get: function() {
return this._strokeAttenuation;
},
set: function(v) {
this._strokeAttenuation = !!v;
this._flagStrokeAttenuation = true;
this._flagLinewidth = true;
}
}
};
// src/shapes/polygon.js
var cos4 = Math.cos;
var sin4 = Math.sin;
var _Polygon = class _Polygon extends Path {
constructor(x, y, radius, sides) {
sides = Math.max(sides || 0, 3);
super();
/**
* @name Two.Polygon#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Polygon#width} needs updating.
*/
__publicField(this, "_flagWidth", false);
/**
* @name Two.Polygon#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Polygon#height} needs updating.
*/
__publicField(this, "_flagHeight", false);
/**
* @name Two.Polygon#_flagSides
* @private
* @property {Boolean} - Determines whether the {@link Two.Polygon#sides} needs updating.
*/
__publicField(this, "_flagSides", false);
/**
* @name Two.Polygon#_radius
* @private
* @see {@link Two.Polygon#radius}
*/
__publicField(this, "_radius", 0);
/**
* @name Two.Polygon#_width
* @private
* @see {@link Two.Polygon#width}
*/
__publicField(this, "_width", 0);
/**
* @name Two.Polygon#_height
* @private
* @see {@link Two.Polygon#height}
*/
__publicField(this, "_height", 0);
/**
* @name Two.Polygon#_sides
* @private
* @see {@link Two.Polygon#sides}
*/
__publicField(this, "_sides", 0);
this._renderer.type = "polygon";
for (let prop in proto17) {
Object.defineProperty(this, prop, proto17[prop]);
}
this.closed = true;
this.automatic = false;
if (typeof radius === "number") {
this.radius = radius;
}
if (typeof sides === "number") {
this.sides = sides;
}
this._update();
if (typeof x === "number") {
this.translation.x = x;
}
if (typeof y === "number") {
this.translation.y = y;
}
}
/**
* @name Two.Polygon.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Polygon} to create a new instance
* @returns {Two.Polygon}
* @description Create a new {@link Two.Polygon} from an object notation of a {@link Two.Polygon}.
* @nota-bene Works in conjunction with {@link Two.Polygon#toObject}
*/
static fromObject(obj) {
const polygon = new _Polygon().copy(obj);
if ("id" in obj) {
polygon.id = obj.id;
}
return polygon;
}
/**
* @name Two.Polygon#copy
* @function
* @param {Two.Polygon} polygon - The reference {@link Two.Polygon}
* @description Copy the properties of one {@link Two.Polygon} onto another.
*/
copy(polygon) {
super.copy.call(this, polygon);
for (let i = 0; i < _Polygon.Properties.length; i++) {
const k = _Polygon.Properties[i];
if (k in polygon && typeof polygon[k] === "number") {
this[k] = polygon[k];
}
}
return this;
}
/**
* @name Two.Polygon#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagWidth || this._flagHeight || this._flagSides) {
const sides = this._sides;
const amount = sides + 1;
let length = this.vertices.length;
if (length > sides) {
this.vertices.splice(sides - 1, length - sides);
length = sides;
}
for (let i = 0; i < amount; i++) {
const pct = (i + 0.5) / sides;
const theta = TWO_PI * pct + Math.PI / 2;
const x = this._width * cos4(theta) / 2;
const y = this._height * sin4(theta) / 2;
if (i >= length) {
this.vertices.push(new Anchor(x, y));
} else {
this.vertices[i].set(x, y);
}
this.vertices[i].command = i === 0 ? Commands.move : Commands.line;
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Polygon#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagWidth = this._flagHeight = this._flagSides = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Polygon#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Polygon}
* @description Create a new instance of {@link Two.Polygon} with the same properties of the current path.
*/
clone(parent) {
const clone = new _Polygon(0, 0, 0, this.sides);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
clone.width = this.width;
clone.height = this.height;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Polygon#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "polygon";
for (let i = 0; i < _Polygon.Properties.length; i++) {
const k = _Polygon.Properties[i];
object[k] = this[k];
}
return object;
}
};
/**
* @name Two.Polygon.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Polygon}.
*/
__publicField(_Polygon, "Properties", ["width", "height", "sides"]);
var Polygon = _Polygon;
var proto17 = {
radius: {
enumerable: true,
get: function() {
return this._radius;
},
set: function(v) {
this._radius = v;
this.width = v * 2;
this.height = v * 2;
}
},
width: {
enumerable: true,
get: function() {
return this._width;
},
set: function(v) {
this._width = v;
this._flagWidth = true;
this._radius = Math.max(this.width, this.height) / 2;
}
},
height: {
enumerable: true,
get: function() {
return this._height;
},
set: function(v) {
this._height = v;
this._flagHeight = true;
this._radius = Math.max(this.width, this.height) / 2;
}
},
sides: {
enumerable: true,
get: function() {
return this._sides;
},
set: function(v) {
this._sides = v;
this._flagSides = true;
}
}
};
// src/shapes/rounded-rectangle.js
var _RoundedRectangle = class _RoundedRectangle extends Path {
constructor(x, y, width, height, radius) {
if (typeof radius === "undefined" && typeof width === "number" && typeof height === "number") {
radius = Math.floor(Math.min(width, height) / 12);
}
const points = [];
for (let i = 0; i < 10; i++) {
points.push(
new Anchor(0, 0, 0, 0, 0, 0, i === 0 ? Commands.move : Commands.curve)
);
}
super(points);
/**
* @name Two.RoundedRectangle#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.RoundedRectangle#width} needs updating.
*/
__publicField(this, "_flagWidth", false);
/**
* @name Two.RoundedRectangle#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.RoundedRectangle#height} needs updating.
*/
__publicField(this, "_flagHeight", false);
/**
* @name Two.RoundedRectangle#_flagRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.RoundedRectangle#radius} needs updating.
*/
__publicField(this, "_flagRadius", false);
/**
* @name Two.RoundedRectangle#_width
* @private
* @see {@link Two.RoundedRectangle#width}
*/
__publicField(this, "_width", 0);
/**
* @name Two.RoundedRectangle#_height
* @private
* @see {@link Two.RoundedRectangle#height}
*/
__publicField(this, "_height", 0);
/**
* @name Two.RoundedRectangle#_radius
* @private
* @see {@link Two.RoundedRectangle#radius}
*/
__publicField(this, "_radius", 12);
this._renderer.type = "rounded-rectangle";
for (let prop in proto18) {
Object.defineProperty(this, prop, proto18[prop]);
}
this.closed = true;
this.automatic = false;
this._renderer.flagRadius = FlagRadius.bind(this);
if (typeof width === "number") {
this.width = width;
}
if (typeof height === "number") {
this.height = height;
}
if (typeof radius === "number" || radius instanceof Vector) {
this.radius = radius;
}
this._update();
if (typeof x === "number") {
this.translation.x = x;
}
if (typeof y === "number") {
this.translation.y = y;
}
}
/**
* @name Two.RoundedRectangle.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.RoundedRectangle} to create a new instance
* @returns {Two.RoundedRectangle}
* @description Create a new {@link Two.RoundedRectangle} from an object notation of a {@link Two.RoundedRectangle}.
* @nota-bene Works in conjunction with {@link Two.RoundedRectangle#toObject}
*/
static fromObject(obj) {
const rectangle = new _RoundedRectangle().copy(obj);
if ("id" in obj) {
rectangle.id = obj.id;
}
return rectangle;
}
/**
* @name Two.RoundedRectangle#copy
* @function
* @param {Two.RoundedRectangle} roundedRectangle - The reference {@link Two.RoundedRectangle}
* @description Copy the properties of one {@link Two.RoundedRectangle} onto another.
*/
copy(roundedRectangle) {
super.copy.call(this, roundedRectangle);
for (let i = 0; i < _RoundedRectangle.Properties.length; i++) {
const k = _RoundedRectangle.Properties[i];
if (k in roundedRectangle) {
const value = roundedRectangle[k];
if (/radius/i.test(k)) {
this[k] = typeof value === "number" || value instanceof Vector ? value : new Vector().copy(value);
} else if (typeof value === "number") {
this[k] = value;
}
}
}
return this;
}
/**
* @name Two.RoundedRectangle#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagWidth || this._flagHeight || this._flagRadius) {
const width = this._width;
const height = this._height;
let rx, ry;
if (this._radius instanceof Vector) {
rx = this._radius.x;
ry = this._radius.y;
} else {
rx = this._radius;
ry = this._radius;
}
let v;
let w = width / 2;
let h = height / 2;
v = this.vertices[0];
v.x = -(w - rx);
v.y = -h;
v = this.vertices[1];
v.x = w - rx;
v.y = -h;
v.controls.left.clear();
v.controls.right.x = rx;
v.controls.right.y = 0;
v = this.vertices[2];
v.x = w;
v.y = -(h - ry);
v.controls.right.clear();
v.controls.left.clear();
v = this.vertices[3];
v.x = w;
v.y = h - ry;
v.controls.left.clear();
v.controls.right.x = 0;
v.controls.right.y = ry;
v = this.vertices[4];
v.x = w - rx;
v.y = h;
v.controls.right.clear();
v.controls.left.clear();
v = this.vertices[5];
v.x = -(w - rx);
v.y = h;
v.controls.left.clear();
v.controls.right.x = -rx;
v.controls.right.y = 0;
v = this.vertices[6];
v.x = -w;
v.y = h - ry;
v.controls.left.clear();
v.controls.right.clear();
v = this.vertices[7];
v.x = -w;
v.y = -(h - ry);
v.controls.left.clear();
v.controls.right.x = 0;
v.controls.right.y = -ry;
v = this.vertices[8];
v.x = -(w - rx);
v.y = -h;
v.controls.left.clear();
v.controls.right.clear();
v = this.vertices[9];
v.copy(this.vertices[8]);
}
super._update.call(this);
return this;
}
/**
* @name Two.RoundedRectangle#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagWidth = this._flagHeight = this._flagRadius = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.RoundedRectangle#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.RoundedRectangle}
* @description Create a new instance of {@link Two.RoundedRectangle} with the same properties of the current path.
*/
clone(parent) {
const width = this.width;
const height = this.height;
const radius = this.radius;
const clone = new _RoundedRectangle(0, 0, width, height, radius);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.RoundedRectangle#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "rounded-rectangle";
for (let i = 0; i < _RoundedRectangle.Properties.length; i++) {
const k = _RoundedRectangle.Properties[i];
object[k] = this[k];
}
object.radius = typeof this.radius === "number" ? this.radius : this.radius.toObject();
return object;
}
};
/**
* @name Two.RoundedRectangle.Properties
* @property {String[]} - A list of properties that are on every {@link Two.RoundedRectangle}.
*/
__publicField(_RoundedRectangle, "Properties", ["width", "height", "radius"]);
var RoundedRectangle = _RoundedRectangle;
var proto18 = {
width: {
enumerable: true,
get: function() {
return this._width;
},
set: function(v) {
this._width = v;
this._flagWidth = true;
}
},
height: {
enumerable: true,
get: function() {
return this._height;
},
set: function(v) {
this._height = v;
this._flagHeight = true;
}
},
radius: {
enumerable: true,
get: function() {
return this._radius;
},
set: function(v) {
if (this._radius instanceof Vector) {
this._radius.unbind(Events.Types.change, this._renderer.flagRadius);
}
this._radius = v;
if (this._radius instanceof Vector) {
this._radius.bind(Events.Types.change, this._renderer.flagRadius);
}
this._flagRadius = true;
}
}
};
function FlagRadius() {
this._flagRadius = true;
}
// src/shapes/star.js
var cos5 = Math.cos;
var sin5 = Math.sin;
var _Star = class _Star extends Path {
constructor(x, y, innerRadius, outerRadius, sides) {
if (arguments.length <= 3) {
outerRadius = innerRadius;
innerRadius = outerRadius / 2;
}
if (typeof sides !== "number" || sides <= 0) {
sides = 5;
}
super();
/**
* @name Two.Star#_flagInnerRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.Star#innerRadius} needs updating.
*/
__publicField(this, "_flagInnerRadius", false);
/**
* @name Two.Star#_flagOuterRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.Star#outerRadius} needs updating.
*/
__publicField(this, "_flagOuterRadius", false);
/**
* @name Two.Star#_flagSides
* @private
* @property {Boolean} - Determines whether the {@link Two.Star#sides} needs updating.
*/
__publicField(this, "_flagSides", false);
/**
* @name Two.Star#_innerRadius
* @private
* @see {@link Two.Star#innerRadius}
*/
__publicField(this, "_innerRadius", 0);
/**
* @name Two.Star#_outerRadius
* @private
* @see {@link Two.Star#outerRadius}
*/
__publicField(this, "_outerRadius", 0);
/**
* @name Two.Star#_sides
* @private
* @see {@link Two.Star#sides}
*/
__publicField(this, "_sides", 0);
this._renderer.type = "star";
for (let prop in proto19) {
Object.defineProperty(this, prop, proto19[prop]);
}
this.closed = true;
this.automatic = false;
if (typeof innerRadius === "number") {
this.innerRadius = innerRadius;
}
if (typeof outerRadius === "number") {
this.outerRadius = outerRadius;
}
if (typeof sides === "number") {
this.sides = sides;
}
this._update();
if (typeof x === "number") {
this.translation.x = x;
}
if (typeof y === "number") {
this.translation.y = y;
}
}
/**
* @name Two.Star.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Star} to create a new instance
* @returns {Two.Star}
* @description Create a new {@link Two.Star} from an object notation of a {@link Two.Star}.
* @nota-bene Works in conjunction with {@link Two.Star#toObject}
*/
static fromObject(obj) {
const star = new _Star().copy(obj);
if ("id" in obj) {
star.id = obj.id;
}
return star;
}
/**
* @name Two.Star#copy
* @function
* @param {Two.Star} star - The reference {@link Two.Star}
* @description Copy the properties of one {@link Two.Star} onto another.
*/
copy(star) {
super.copy.call(this, star);
for (let i = 0; i < _Star.Properties.length; i++) {
const k = _Star.Properties[i];
if (k in star && typeof star[k] === "number") {
this[k] = star[k];
}
}
return this;
}
/**
* @name Two.Star#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagInnerRadius || this._flagOuterRadius || this._flagSides) {
const sides = this._sides * 2;
const amount = sides + 1;
let length = this.vertices.length;
if (length > sides) {
this.vertices.splice(sides - 1, length - sides);
length = sides;
}
for (let i = 0; i < amount; i++) {
const pct = (i + 0.5) / sides;
const theta = TWO_PI * pct;
const r = (!(i % 2) ? this._innerRadius : this._outerRadius) / 2;
const x = r * cos5(theta);
const y = r * sin5(theta);
if (i >= length) {
this.vertices.push(new Anchor(x, y));
} else {
this.vertices[i].set(x, y);
}
this.vertices[i].command = i === 0 ? Commands.move : Commands.line;
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Star#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagInnerRadius = this._flagOuterRadius = this._flagSides = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Star#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Star}
* @description Create a new instance of {@link Two.Star} with the same properties of the current path.
*/
clone(parent) {
const ir = this.innerRadius;
const or = this.outerRadius;
const sides = this.sides;
const clone = new _Star(0, 0, ir, or, sides);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Star#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "star";
for (let i = 0; i < _Star.Properties.length; i++) {
const k = _Star.Properties[i];
object[k] = this[k];
}
return object;
}
};
/**
* @name Two.Star.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Star}.
*/
__publicField(_Star, "Properties", ["innerRadius", "outerRadius", "sides"]);
var Star = _Star;
var proto19 = {
innerRadius: {
enumerable: true,
get: function() {
return this._innerRadius;
},
set: function(v) {
this._innerRadius = v;
this._flagInnerRadius = true;
}
},
outerRadius: {
enumerable: true,
get: function() {
return this._outerRadius;
},
set: function(v) {
this._outerRadius = v;
this._flagOuterRadius = true;
}
},
sides: {
enumerable: true,
get: function() {
return this._sides;
},
set: function(v) {
this._sides = v;
this._flagSides = true;
}
}
};
// src/text.js
var canvas;
var min2 = Math.min;
var max2 = Math.max;
if (root.document) {
canvas = document.createElement("canvas");
}
var _Text = class _Text extends Shape {
constructor(message, x, y, styles) {
super();
/**
* @name Two.Text#_flagValue
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#value} need updating.
*/
__publicField(this, "_flagValue", true);
/**
* @name Two.Text#_flagFamily
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#family} need updating.
*/
__publicField(this, "_flagFamily", true);
/**
* @name Two.Text#_flagSize
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#size} need updating.
*/
__publicField(this, "_flagSize", true);
/**
* @name Two.Text#_flagLeading
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#leading} need updating.
*/
__publicField(this, "_flagLeading", true);
/**
* @name Two.Text#_flagAlignment
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#alignment} need updating.
*/
__publicField(this, "_flagAlignment", true);
/**
* @name Two.Text#_flagBaseline
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#baseline} need updating.
*/
__publicField(this, "_flagBaseline", true);
/**
* @name Two.Text#_flagStyle
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#style} need updating.
*/
__publicField(this, "_flagStyle", true);
/**
* @name Two.Text#_flagWeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#weight} need updating.
*/
__publicField(this, "_flagWeight", true);
/**
* @name Two.Text#_flagDecoration
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#decoration} need updating.
*/
__publicField(this, "_flagDecoration", true);
/**
* @name Two.Text#_flagFill
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#fill} need updating.
*/
__publicField(this, "_flagFill", true);
/**
* @name Two.Text#_flagStroke
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#stroke} need updating.
*/
__publicField(this, "_flagStroke", true);
/**
* @name Two.Text#_flagLinewidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#linewidth} need updating.
*/
__publicField(this, "_flagLinewidth", true);
/**
* @name Two.Text#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#opacity} need updating.
*/
__publicField(this, "_flagOpacity", true);
/**
* @name Two.Text#_flagVisible
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#visible} need updating.
*/
__publicField(this, "_flagVisible", true);
/**
* @name Two.Text#_flagMask
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#mask} needs updating.
*/
__publicField(this, "_flagMask", false);
/**
* @name Two.Text#_flagClip
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#clip} needs updating.
*/
__publicField(this, "_flagClip", false);
/**
* @name Two.Text#_flagDirection
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#direction} needs updating.
*/
__publicField(this, "_flagDirection", true);
/**
* @name Two.Text#_flagStrokeAttenuation
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#strokeAttenuation} needs updating.
*/
__publicField(this, "_flagStrokeAttenuation", true);
// Underlying Properties
/**
* @name Two.Text#value
* @property {String} - The characters to be rendered to the the screen. Referred to in the documentation sometimes as the `message`.
*/
__publicField(this, "_value", "");
/**
* @name Two.Text#family
* @property {String} - The font family Two.js should attempt to register for rendering. The default value is `'sans-serif'`. Comma separated font names can be supplied as a "stack", similar to the CSS implementation of `font-family`.
*/
__publicField(this, "_family", "sans-serif");
/**
* @name Two.Text#size
* @property {Number} - The font size in Two.js point space. Defaults to `13`.
*/
__publicField(this, "_size", 13);
/**
* @name Two.Text#leading
* @property {Number} - The height between lines measured from base to base in Two.js point space. Defaults to `17`.
*/
__publicField(this, "_leading", 17);
/**
* @name Two.Text#alignment
* @property {String} - Alignment of text in relation to {@link Two.Text#translation}'s coordinates. Possible values include `'left'`, `'center'`, `'right'`. Defaults to `'center'`.
*/
__publicField(this, "_alignment", "center");
/**
* @name Two.Text#baseline
* @property {String} - The vertical aligment of the text in relation to {@link Two.Text#translation}'s coordinates. Possible values include `'top'`, `'middle'`, `'bottom'`, and `'baseline'`. Defaults to `'baseline'`.
* @nota-bene In headless environments where the canvas is based on {@link https://github.com/Automattic/node-canvas}, `baseline` seems to be the only valid property.
*/
__publicField(this, "_baseline", "middle");
/**
* @name Two.Text#style
* @property {String} - The font's style. Possible values include '`normal`', `'italic'`. Defaults to `'normal'`.
*/
__publicField(this, "_style", "normal");
/**
* @name Two.Text#weight
* @property {Number} - A number at intervals of 100 to describe the font's weight. This compatibility varies with the typeface's variant weights. Larger values are bolder. Smaller values are thinner. Defaults to `'500'`.
*/
__publicField(this, "_weight", 500);
/**
* @name Two.Text#decoration
* @property {String} - String to delineate whether text should be decorated with for instance an `'underline'`. Defaults to `'none'`.
*/
__publicField(this, "_decoration", "none");
/**
* @name Two.Text#direction
* @property {String} - String to determine what direction the text should run. Possibly values are `'ltr'` for left-to-right and `'rtl'` for right-to-left. Defaults to `'ltr'`.
*/
__publicField(this, "_direction", "ltr");
/**
* @name Two.Text#fill
* @property {(String|Two.Gradient|Two.Texture)} - The value of what the text object should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
__publicField(this, "_fill", "#000");
/**
* @name Two.Text#stroke
* @property {(String|Two.Gradient|Two.Texture)} - The value of what the text object should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
__publicField(this, "_stroke", "none");
/**
* @name Two.Text#linewidth
* @property {Number} - The thickness in pixels of the stroke.
*/
__publicField(this, "_linewidth", 1);
/**
* @name Two.Text#opacity
* @property {Number} - The opaqueness of the text object.
* @nota-bene Can be used in conjunction with CSS Colors that have an alpha value.
*/
__publicField(this, "_opacity", 1);
/**
* @name Two.Text#visible
* @property {Boolean} - Display the text object or not.
* @nota-bene For {@link Two.CanvasRenderer} and {@link Two.WebGLRenderer} when set to false all updating is disabled improving performance dramatically with many objects in the scene.
*/
__publicField(this, "_visible", true);
/**
* @name Two.Text#mask
* @property {Two.Shape} - The shape whose alpha property becomes a clipping area for the text.
* @nota-bene This property is currently not working because of SVG spec issues found here {@link https://code.google.com/p/chromium/issues/detail?id=370951}.
*/
__publicField(this, "_mask", null);
/**
* @name Two.Text#clip
* @property {Two.Shape} - Object to define clipping area.
* @nota-bene This property is currently not working because of SVG spec issues found here {@link https://code.google.com/p/chromium/issues/detail?id=370951}.
*/
__publicField(this, "_clip", false);
/**
* @name Two.Text#_dashes
* @private
* @see {@link Two.Text#dashes}
*/
__publicField(this, "_dashes", null);
/**
* @name Two.Text#_strokeAttenuation
* @private
* @see {@link Two.Text#strokeAttenuation}
*/
__publicField(this, "_strokeAttenuation", true);
for (let prop in proto20) {
Object.defineProperty(this, prop, proto20[prop]);
}
this._renderer.type = "text";
this._renderer.flagFill = FlagFill2.bind(this);
this._renderer.flagStroke = FlagStroke2.bind(this);
this.value = message;
if (typeof x === "number") {
this.translation.x = x;
}
if (typeof y === "number") {
this.translation.y = y;
}
this.dashes = [];
this.dashes.offset = 0;
if (!_.isObject(styles)) {
return this;
}
for (let i = 0; i < _Text.Properties.length; i++) {
const property = _Text.Properties[i];
if (property in styles) {
this[property] = styles[property];
}
}
}
/**
*
* @name Two.Measure
* @function
* @param {Two.Text} [text] - The instance of {@link Two.Text} to measure.
* @returns {Object} - The width and height of the {@link Two.Text} instance.
*/
static Measure(text) {
if (canvas) {
const ctx = canvas.getContext("2d");
ctx.font = [
text._style,
text._weight,
`${text._size}px/${text._leading}px`,
text._family
].join(" ");
const metrics = ctx.measureText(text.value, 0, 0);
const height = metrics.actualBoundingBoxDescent + metrics.actualBoundingBoxAscent;
return {
width: metrics.width,
height
};
} else {
const width = this.value.length * this.size * _Text.Ratio;
const height = this.leading;
console.warn(
"Two.Text: unable to accurately measure text, so using an approximation."
);
return {
width,
height
};
}
}
/**
* @name Two.Text.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Text} to create a new instance
* @returns {Two.Text}
* @description Create a new {@link Two.Text} from an object notation of a {@link Two.Text}.
* @nota-bene Works in conjunction with {@link Two.Text#toObject}
*/
static fromObject(obj) {
const fill = typeof obj.fill === "string" ? obj.fill : getEffectFromObject(obj.fill);
const stroke = typeof obj.stroke === "string" ? obj.stroke : getEffectFromObject(obj.stroke);
const text = new _Text().copy(__spreadProps(__spreadValues({}, obj), { fill, stroke }));
if ("id" in obj) {
text.id = obj.id;
}
return text;
}
/**
* @name Two.Text#copy
* @function
* @param {Two.Text} text
* @description Copy the properties of one {@link Two.Text} onto another.
*/
copy(text) {
super.copy.call(this, text);
for (let i = 0; i < _Text.Properties.length; i++) {
const k = _Text.Properties[i];
if (k in text) {
this[k] = text[k];
}
}
return this;
}
/**
* @name Two.Text#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Text}
* @description Create a new instance of {@link Two.Text} with the same properties of the current text object.
*/
clone(parent) {
const clone = new _Text(this.value);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
for (let i = 0; i < _Text.Properties.length; i++) {
const prop = _Text.Properties[i];
clone[prop] = this[prop];
}
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
if (parent) {
parent.add(clone);
}
return clone._update();
}
/**
* @name Two.Text#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the text object.
* @nota-bene Works in conjunction with {@link Two.Text.fromObject}
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = "text";
for (let i = 0; i < _Text.Properties.length; i++) {
const prop = _Text.Properties[i];
result[prop] = this[prop];
}
return result;
}
/**
* @name Two.Text#dispose
* @function
* @returns {Two.Text}
* @description Release the text's renderer resources and detach all events.
* This method disposes fill and stroke effects (calling dispose() on
* Gradients and Textures for thorough cleanup) while preserving the
* renderer type for potential re-attachment to a new renderer.
*/
dispose() {
super.dispose();
if (typeof this.fill === "object" && typeof this.fill.dispose === "function") {
this.fill.dispose();
} else if (typeof this.fill === "object" && typeof this.fill.unbind === "function") {
this.fill.unbind();
}
if (typeof this.stroke === "object" && typeof this.stroke.dispose === "function") {
this.stroke.dispose();
} else if (typeof this.stroke === "object" && typeof this.stroke.unbind === "function") {
this.stroke.unbind();
}
return this;
}
/**
* @name Two.Text#noFill
* @function
* @description Short hand method to set fill to `none`.
*/
noFill() {
this.fill = "none";
return this;
}
/**
* @name Two.Text#noStroke
* @function
* @description Short hand method to set stroke to `none`.
*/
noStroke() {
this.stroke = "none";
this.linewidth = 0;
return this;
}
// A shim to not break `getBoundingClientRect` calls.
// TODO: Implement a way to calculate proper bounding
// boxes of `Two.Text`.
/**
* @name Two.Text#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the text object.
*/
getBoundingClientRect(shallow) {
let matrix;
let left, right, top, bottom;
this._update(true);
matrix = shallow ? this.matrix : this.worldMatrix;
const { width, height } = _Text.Measure(this);
const border = (this._linewidth || 0) / 2;
switch (this.alignment) {
case "left":
left = -border;
right = width + border;
break;
case "right":
left = -(width + border);
right = border;
break;
default:
left = -(width / 2 + border);
right = width / 2 + border;
}
switch (this.baseline) {
case "middle":
top = -(height / 2 + border);
bottom = height / 2 + border;
break;
default:
top = -(height + border);
bottom = border;
}
const [ax, ay] = matrix.multiply(left, top);
const [bx, by] = matrix.multiply(left, bottom);
const [cx, cy] = matrix.multiply(right, top);
const [dx, dy] = matrix.multiply(right, bottom);
top = min2(ay, by, cy, dy);
left = min2(ax, bx, cx, dx);
right = max2(ax, bx, cx, dx);
bottom = max2(ay, by, cy, dy);
return {
top,
left,
right,
bottom,
width: right - left,
height: bottom - top
};
}
/**
* @name Two.Text#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
super.flagReset.call(this);
this._flagValue = this._flagFamily = this._flagSize = this._flagLeading = this._flagAlignment = this._flagFill = this._flagStroke = this._flagLinewidth = this._flagOpacity = this._flagVisible = this._flagClip = this._flagDecoration = this._flagClassName = this._flagBaseline = this._flagWeight = this._flagStyle = this._flagDirection = false;
return this;
}
};
/**
* @name Two.Text.Ratio
* @property {Number} - Approximate aspect ratio of a typeface's character width to height.
*/
__publicField(_Text, "Ratio", 0.6);
/**
* @name Two.Text.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Text}.
*/
__publicField(_Text, "Properties", [
"value",
"family",
"size",
"leading",
"alignment",
"linewidth",
"style",
"weight",
"decoration",
"direction",
"baseline",
"opacity",
"visible",
"fill",
"stroke",
"dashes",
"strokeAttenuation"
]);
var Text = _Text;
var proto20 = {
value: {
enumerable: true,
get: function() {
return this._value;
},
set: function(v) {
this._value = v;
this._flagValue = true;
}
},
family: {
enumerable: true,
get: function() {
return this._family;
},
set: function(v) {
this._family = v;
this._flagFamily = true;
}
},
size: {
enumerable: true,
get: function() {
return this._size;
},
set: function(v) {
this._size = v;
this._flagSize = true;
}
},
leading: {
enumerable: true,
get: function() {
return this._leading;
},
set: function(v) {
this._leading = v;
this._flagLeading = true;
}
},
alignment: {
enumerable: true,
get: function() {
return this._alignment;
},
set: function(v) {
this._alignment = v;
this._flagAlignment = true;
}
},
linewidth: {
enumerable: true,
get: function() {
return this._linewidth;
},
set: function(v) {
this._linewidth = v;
this._flagLinewidth = true;
}
},
style: {
enumerable: true,
get: function() {
return this._style;
},
set: function(v) {
this._style = v;
this._flagStyle = true;
}
},
weight: {
enumerable: true,
get: function() {
return this._weight;
},
set: function(v) {
this._weight = v;
this._flagWeight = true;
}
},
decoration: {
enumerable: true,
get: function() {
return this._decoration;
},
set: function(v) {
this._decoration = v;
this._flagDecoration = true;
}
},
direction: {
enumerable: true,
get: function() {
return this._direction;
},
set: function(v) {
this._direction = v;
this._flagDirection = true;
}
},
baseline: {
enumerable: true,
get: function() {
return this._baseline;
},
set: function(v) {
this._baseline = v;
this._flagBaseline = true;
}
},
opacity: {
enumerable: true,
get: function() {
return this._opacity;
},
set: function(v) {
this._opacity = v;
this._flagOpacity = true;
}
},
visible: {
enumerable: true,
get: function() {
return this._visible;
},
set: function(v) {
this._visible = v;
this._flagVisible = true;
}
},
fill: {
enumerable: true,
get: function() {
return this._fill;
},
set: function(f) {
if (this._fill instanceof Gradient || this._fill instanceof LinearGradient || this._fill instanceof RadialGradient || this._fill instanceof Texture) {
this._fill.unbind(Events.Types.change, this._renderer.flagFill);
}
this._fill = f;
this._flagFill = true;
if (this._fill instanceof Gradient || this._fill instanceof LinearGradient || this._fill instanceof RadialGradient || this._fill instanceof Texture) {
this._fill.bind(Events.Types.change, this._renderer.flagFill);
}
}
},
stroke: {
enumerable: true,
get: function() {
return this._stroke;
},
set: function(f) {
if (this._stroke instanceof Gradient || this._stroke instanceof LinearGradient || this._stroke instanceof RadialGradient || this._stroke instanceof Texture) {
this._stroke.unbind(Events.Types.change, this._renderer.flagStroke);
}
this._stroke = f;
this._flagStroke = true;
if (this._stroke instanceof Gradient || this._stroke instanceof LinearGradient || this._stroke instanceof RadialGradient || this._stroke instanceof Texture) {
this._stroke.bind(Events.Types.change, this._renderer.flagStroke);
}
}
},
mask: {
enumerable: true,
get: function() {
return this._mask;
},
set: function(v) {
this._mask = v;
this._flagMask = true;
if (_.isObject(v) && !v.clip) {
v.clip = true;
}
}
},
clip: {
enumerable: true,
get: function() {
return this._clip;
},
set: function(v) {
this._clip = v;
this._flagClip = true;
}
},
dashes: {
enumerable: true,
get: function() {
return this._dashes;
},
set: function(v) {
if (typeof v.offset !== "number") {
v.offset = this.dashes && this._dashes.offset || 0;
}
this._dashes = v;
}
},
/**
* @name Two.Text#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
* @description When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
*/
strokeAttenuation: {
enumerable: true,
get: function() {
return this._strokeAttenuation;
},
set: function(v) {
this._strokeAttenuation = !!v;
this._flagStrokeAttenuation = true;
this._flagLinewidth = true;
}
}
};
function FlagFill2() {
this._flagFill = true;
}
function FlagStroke2() {
this._flagStroke = true;
}
// src/effects/image-sequence.js
var _ImageSequence = class _ImageSequence extends Rectangle {
constructor(src, ox, oy, frameRate) {
super(ox, oy, 0, 0);
/**
* @name Two.ImageSequence#_flagTextures
* @private
* @property {Boolean} - Determines whether the {@link Two.ImageSequence#textures} need updating.
*/
__publicField(this, "_flagTextures", false);
/**
* @name Two.ImageSequence#_flagFrameRate
* @private
* @property {Boolean} - Determines whether the {@link Two.ImageSequence#frameRate} needs updating.
*/
__publicField(this, "_flagFrameRate", false);
/**
* @name Two.ImageSequence#_flagIndex
* @private
* @property {Boolean} - Determines whether the {@link Two.ImageSequence#index} needs updating.
*/
__publicField(this, "_flagIndex", false);
// Private variables
/**
* @name Two.ImageSequence#_amount
* @private
* @property {Number} - Number of frames for a given {@link Two.ImageSequence}.
*/
__publicField(this, "_amount", 1);
/**
* @name Two.ImageSequence#_duration
* @private
* @property {Number} - Number of milliseconds a {@link Two.ImageSequence}.
*/
__publicField(this, "_duration", 0);
/**
* @name Two.ImageSequence#_index
* @private
* @property {Number} - The current frame the {@link Two.ImageSequence} is currently displaying.
*/
__publicField(this, "_index", 0);
/**
* @name Two.ImageSequence#_startTime
* @private
* @property {Milliseconds} - Epoch time in milliseconds of when the {@link Two.ImageSequence} started.
*/
__publicField(this, "_startTime", 0);
/**
* @name Two.ImageSequence#_playing
* @private
* @property {Boolean} - Dictates whether the {@link Two.ImageSequence} is animating or not.
*/
__publicField(this, "_playing", false);
/**
* @name Two.ImageSequence#_firstFrame
* @private
* @property {Number} - The frame the {@link Two.ImageSequence} should start with.
*/
__publicField(this, "_firstFrame", 0);
/**
* @name Two.ImageSequence#_lastFrame
* @private
* @property {Number} - The frame the {@link Two.ImageSequence} should end with.
*/
__publicField(this, "_lastFrame", 0);
/**
* @name Two.ImageSequence#_playing
* @private
* @property {Boolean} - Dictates whether the {@link Two.ImageSequence} should loop or not.
*/
__publicField(this, "_loop", true);
// Exposed through getter-setter
/**
* @name Two.ImageSequence#_textures
* @private
* @see {@link Two.ImageSequence#textures}
*/
__publicField(this, "_textures", null);
/**
* @name Two.ImageSequence#_frameRate
* @private
* @see {@link Two.ImageSequence#frameRate}
*/
__publicField(this, "_frameRate", 0);
/**
* @name Two.ImageSequence#_origin
* @private
* @see {@link Two.ImageSequence#origin}
*/
__publicField(this, "_origin", null);
this._renderer.type = "image-sequence";
for (let prop in proto21) {
Object.defineProperty(this, prop, proto21[prop]);
}
this._renderer.flagTextures = FlagTextures.bind(this);
this._renderer.bindTextures = BindTextures.bind(this);
this._renderer.unbindTextures = UnbindTextures.bind(this);
this.noStroke();
this.noFill();
if (Array.isArray(src)) {
this.textures = src.map(GenerateTexture.bind(this));
} else if (typeof src === "string") {
this.textures = [GenerateTexture(src)];
}
this.origin = new Vector();
this._update();
if (typeof frameRate === "number") {
this.frameRate = frameRate;
} else {
this.frameRate = _ImageSequence.DefaultFrameRate;
}
this.index = 0;
}
/**
* @name Two.ImageSequence.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.ImageSequence} to create a new instance
* @returns {Two.ImageSequence}
* @description Create a new {@link Two.ImageSequence} from an object notation of a {@link Two.ImageSequence}.
* @nota-bene Works in conjunction with {@link Two.ImageSequence#toObject}
*/
static fromObject(obj) {
const sequence = new _ImageSequence().copy(obj);
if ("id" in obj) {
sequence.id = obj.id;
}
return sequence;
}
/**
* @name Two.ImageSequence#copy
* @function
* @param {Two.ImageSequence} imageSequence - The reference {@link Two.ImageSequence}
* @description Copy the properties of one {@link Two.ImageSequence} onto another.
*/
copy(imageSequence) {
super.copy.call(this, imageSequence);
for (let i = 0; i < _ImageSequence.Properties.length; i++) {
const k = _ImageSequence.Properties[i];
if (k in imageSequence) {
this[k] = imageSequence[k];
}
}
return this;
}
/**
* @name Two.ImageSequence#play
* @function
* @param {Number} [firstFrame=0] - The index of the frame to start the animation with.
* @param {Number} [lastFrame] - The index of the frame to end the animation with. Defaults to the last item in the {@link Two.ImageSequence#textures}.
* @param {Function} [onLastFrame] - Optional callback function to be triggered after playing the last frame. This fires multiple times when the image sequence is looped.
* @description Initiate animation playback of a {@link Two.ImageSequence}.
*/
play(firstFrame, lastFrame, onLastFrame) {
this._playing = true;
this._firstFrame = 0;
this._lastFrame = this.amount - 1;
this._startTime = _.performance.now();
if (typeof firstFrame === "number") {
this._firstFrame = firstFrame;
}
if (typeof lastFrame === "number") {
this._lastFrame = lastFrame;
}
if (typeof onLastFrame === "function") {
this._onLastFrame = onLastFrame;
} else {
delete this._onLastFrame;
}
if (this._index !== this._firstFrame) {
this._startTime -= 1e3 * Math.abs(this._index - this._firstFrame) / this._frameRate;
}
return this;
}
/**
* @name Two.ImageSequence#pause
* @function
* @description Halt animation playback of a {@link Two.ImageSequence}.
*/
pause() {
this._playing = false;
return this;
}
/**
* @name Two.ImageSequence#stop
* @function
* @description Halt animation playback of a {@link Two.ImageSequence} and set the current frame back to the first frame.
*/
stop() {
this._playing = false;
this._index = this._firstFrame;
return this;
}
/**
* @name Two.ImageSequence#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.ImageSequence}
* @description Create a new instance of {@link Two.ImageSequence} with the same properties of the current image sequence.
*/
clone(parent) {
const clone = new _ImageSequence(
this.textures,
this.translation.x,
this.translation.y,
this.frameRate
);
clone._loop = this._loop;
if (this._playing) {
clone.play();
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.ImageSequence#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "image-sequence";
object.textures = this.textures.map(function(texture) {
return texture.toObject();
});
object.frameRate = this.frameRate;
object.index = this.index;
object.firstFrame = this.firstFrame;
object.lastFrame = this.lastFrame;
object.loop = this.loop;
return object;
}
/**
* @name Two.ImageSequence#dispose
* @function
* @returns {Two.ImageSequence}
* @description Release the image sequence's renderer resources and detach all events.
* This method stops any running animation, clears animation callbacks, unbinds
* textures collection events, and disposes individual textures (calling dispose()
* for thorough cleanup) while preserving the renderer type for potential
* re-attachment to a new renderer.
*/
dispose() {
super.dispose();
if (this._playing) {
this._playing = false;
}
this._onLastFrame = null;
if (this.textures && typeof this.textures.unbind === "function") {
try {
this.textures.unbind();
} catch (e) {
}
}
if (this.textures) {
for (let i = 0; i < this.textures.length; i++) {
const texture = this.textures[i];
if (typeof texture.dispose === "function") {
texture.dispose();
} else if (typeof texture.unbind === "function") {
texture.unbind();
}
}
}
return this;
}
/**
* @name Two.ImageSequence#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
const effect = this._textures;
let width, height, elapsed, amount, duration, texture;
let index, frames;
if (effect) {
if (this._flagTextures) {
this._amount = effect.length;
}
if (this._flagFrameRate) {
this._duration = 1e3 * this._amount / this._frameRate;
}
if (this._playing && this._frameRate > 0) {
amount = this._amount;
if (_.isNaN(this._lastFrame)) {
this._lastFrame = amount - 1;
}
elapsed = _.performance.now() - this._startTime;
frames = this._lastFrame + 1;
duration = 1e3 * (frames - this._firstFrame) / this._frameRate;
if (this._loop) {
elapsed = elapsed % duration;
} else {
elapsed = Math.min(elapsed, duration);
}
index = lerp(this._firstFrame, frames, elapsed / duration);
index = Math.floor(index);
if (index !== this._index) {
this._index = index;
texture = effect[this._index];
if (texture.loaded) {
width = texture.image.width;
height = texture.image.height;
if (this.width !== width) {
this.width = width;
}
if (this.height !== height) {
this.height = height;
}
this.fill = texture;
if (index >= this._lastFrame - 1 && this._onLastFrame) {
this._onLastFrame();
}
}
}
} else if (this._flagIndex || !(this.fill instanceof Texture)) {
texture = effect[this._index];
if (texture.loaded) {
width = texture.image.width;
height = texture.image.height;
if (this.width !== width) {
this.width = width;
}
if (this.height !== height) {
this.height = height;
}
}
this.fill = texture;
}
}
super._update.call(this);
return this;
}
/**
* @name Two.ImageSequence#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagTextures = this._flagFrameRate = false;
super.flagReset.call(this);
return this;
}
};
/**
* @name Two.ImageSequence.Properties
* @property {String[]} - A list of properties that are on every {@link Two.ImageSequence}.
*/
__publicField(_ImageSequence, "Properties", [
"textures",
"frameRate",
"index",
"firstFrame",
"lastFrame",
"loop"
]);
/**
* @name Two.ImageSequence.DefaultFrameRate
* @property The default frame rate that {@link Two.ImageSequence#frameRate} is set to when instantiated.
*/
__publicField(_ImageSequence, "DefaultFrameRate", 30);
var ImageSequence = _ImageSequence;
var proto21 = {
frameRate: {
enumerable: true,
get: function() {
return this._frameRate;
},
set: function(v) {
this._frameRate = v;
this._flagFrameRate = true;
}
},
index: {
enumerable: true,
get: function() {
return this._index;
},
set: function(v) {
this._index = v;
this._flagIndex = true;
}
},
textures: {
enumerable: true,
get: function() {
return this._textures;
},
set: function(textures) {
const bindTextures = this._renderer.bindTextures;
const unbindTextures = this._renderer.unbindTextures;
if (this._textures) {
this._textures.unbind(Events.Types.insert, bindTextures).unbind(Events.Types.remove, unbindTextures);
}
this._textures = new Collection((textures || []).slice(0));
this._textures.bind(Events.Types.insert, bindTextures).bind(Events.Types.remove, unbindTextures);
bindTextures(this._textures);
}
},
firstFrame: {
enumerable: true,
get: function() {
return this._firstFrame;
},
set: function(v) {
this._firstFrame = v;
}
},
lastFrame: {
enumerable: true,
get: function() {
return this._lastFrame;
},
set: function(v) {
this._lastFrame = v;
}
},
loop: {
enumerable: true,
get: function() {
return this._loop;
},
set: function(v) {
this._loop = !!v;
}
}
};
function FlagTextures() {
this._flagTextures = true;
}
function BindTextures(items) {
let i = items.length;
while (i--) {
items[i].bind(Events.Types.change, this._renderer.flagTextures);
}
this._renderer.flagTextures();
}
function UnbindTextures(items) {
let i = items.length;
while (i--) {
items[i].unbind(Events.Types.change, this._renderer.flagTextures);
}
this._renderer.flagTextures();
}
function GenerateTexture(obj) {
if (obj instanceof Texture) {
return obj;
} else if (typeof obj === "string") {
return new Texture(obj);
}
}
// src/group.js
var min3 = Math.min;
var max3 = Math.max;
var cache = {
getShapesAtPoint: {
results: [],
hitOptions: {},
context: {
x: 0,
y: 0,
visibleOnly: true,
results: null
},
single: [],
output: [],
empty: []
}
};
var _Group = class _Group extends Shape {
constructor(children) {
super();
/**
* @name Two.Group#_flagAdditions
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#additions} needs updating.
*/
__publicField(this, "_flagAdditions", false);
/**
* @name Two.Group#_flagSubtractions
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#subtractions} needs updating.
*/
__publicField(this, "_flagSubtractions", false);
/**
* @name Two.Group#_flagOrder
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#order} needs updating.
*/
__publicField(this, "_flagOrder", false);
/**
* @name Two.Group#_flagVisible
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#visible} needs updating.
*/
/**
* @name Two.Group#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#opacity} needs updating.
*/
__publicField(this, "_flagOpacity", true);
/**
* @name Two.Group#_flagBeginning
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#beginning} needs updating.
*/
__publicField(this, "_flagBeginning", false);
/**
* @name Two.Group#_flagEnding
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#ending} needs updating.
*/
__publicField(this, "_flagEnding", false);
/**
* @name Two.Group#_flagLength
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#length} needs updating.
*/
__publicField(this, "_flagLength", false);
/**
* @name Two.Group#_flagMask
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#mask} needs updating.
*/
__publicField(this, "_flagMask", false);
// Underlying Properties
/**
* @name Two.Group#fill
* @property {(String|Two.Gradient|Two.Texture)} - The value of what all child shapes should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
__publicField(this, "_fill", "#fff");
/**
* @name Two.Group#stroke
* @property {(String|Two.Gradient|Two.Texture)} - The value of what all child shapes should be outlined in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
__publicField(this, "_stroke", "#000");
/**
* @name Two.Group#linewidth
* @property {Number} - The thickness in pixels of the stroke for all child shapes.
*/
__publicField(this, "_linewidth", 1);
/**
* @name Two.Group#opacity
* @property {Number} - The opaqueness of all child shapes.
* @nota-bene Becomes multiplied by the individual child's opacity property.
*/
__publicField(this, "_opacity", 1);
/**
* @name Two.Group#visible
* @property {Boolean} - Display the path or not.
* @nota-bene For {@link Two.CanvasRenderer} and {@link Two.WebGLRenderer} when set to false all updating is disabled improving performance dramatically with many objects in the scene.
*/
__publicField(this, "_visible", true);
/**
* @name Two.Group#cap
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeLinecapProperty}
*/
__publicField(this, "_cap", "round");
/**
* @name Two.Group#join
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeLinejoinProperty}
*/
__publicField(this, "_join", "round");
/**
* @name Two.Group#miter
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeMiterlimitProperty}
*/
__publicField(this, "_miter", 4);
/**
* @name Two.Group#closed
* @property {Boolean} - Determines whether a final line is drawn between the final point in the `vertices` array and the first point of all child shapes.
*/
__publicField(this, "_closed", true);
/**
* @name Two.Group#curved
* @property {Boolean} - When the child's path is `automatic = true` this boolean determines whether the lines between the points are curved or not.
*/
__publicField(this, "_curved", false);
/**
* @name Two.Group#automatic
* @property {Boolean} - Determines whether or not Two.js should calculate curves, lines, and commands automatically for you or to let the developer manipulate them for themselves.
*/
__publicField(this, "_automatic", true);
/**
* @name Two.Group#beginning
* @property {Number} - Number between zero and one to state the beginning of where the path is rendered.
* @description {@link Two.Group#beginning} is a percentage value that represents at what percentage into all child shapes should the renderer start drawing.
* @nota-bene This is great for animating in and out stroked paths in conjunction with {@link Two.Group#ending}.
*/
__publicField(this, "_beginning", 0);
/**
* @name Two.Group#ending
* @property {Number} - Number between zero and one to state the ending of where the path is rendered.
* @description {@link Two.Group#ending} is a percentage value that represents at what percentage into all child shapes should the renderer start drawing.
* @nota-bene This is great for animating in and out stroked paths in conjunction with {@link Two.Group#beginning}.
*/
__publicField(this, "_ending", 1);
/**
* @name Two.Group#length
* @property {Number} - The sum of distances between all child lengths.
*/
__publicField(this, "_length", 0);
/**
* @name Two.Group#mask
* @property {Two.Shape} - The Two.js object to clip from a group's rendering.
*/
__publicField(this, "_mask", null);
/**
* @name Two.Group#_strokeAttenuation
* @private
* @see {@link Two.Group#strokeAttenuation}
*/
__publicField(this, "_strokeAttenuation", true);
for (let prop in proto22) {
Object.defineProperty(this, prop, proto22[prop]);
}
this._renderer.type = "group";
this.additions = [];
this.subtractions = [];
this.children = Array.isArray(children) ? children : Array.prototype.slice.call(arguments);
}
/**
* @name Two.Group.InsertChildren
* @function
* @param {Two.Shape[]} children - The objects to be inserted.
* @description Cached method to let renderers know children have been added to a {@link Two.Group}.
*/
static InsertChildren(children) {
for (let i = 0; i < children.length; i++) {
replaceParent.call(this, children[i], this);
}
}
/**
* @name Two.Group.RemoveChildren
* @function
* @param {Two.Shape[]} children - The objects to be removed.
* @description Cached method to let renderers know children have been removed from a {@link Two.Group}.
*/
static RemoveChildren(children) {
for (let i = 0; i < children.length; i++) {
replaceParent.call(this, children[i]);
}
}
/**
* @name Two.Group.OrderChildren
* @function
* @description Cached method to let renderers know order has been updated on a {@link Two.Group}.
*/
static OrderChildren(children) {
this._flagOrder = true;
}
static fromObject(obj) {
const group = new _Group();
for (let i = 0; i < _Group.Properties.length; i++) {
const k = _Group.Properties[i];
if (k in obj) {
if (/(fill|stroke)/i.test(k)) {
group[k] = typeof obj[k] === "string" ? obj[k] : getEffectFromObject(obj[k]);
} else {
group[k] = obj[k];
}
}
}
if ("mask" in obj) {
group.mask = getShapeFromObject(obj.mask);
}
if ("id" in obj) {
group.id = obj.id;
}
group.children = obj.children.map(getShapeFromObject);
return group;
function getShapeFromObject(child) {
if (child && child.renderer) {
switch (child.renderer.type) {
case "arc-segment":
return ArcSegment.fromObject(child);
case "circle":
return Circle.fromObject(child);
case "element":
return Element.fromObject(child);
case "ellipse":
return Ellipse.fromObject(child);
case "group":
return _Group.fromObject(child);
case "image":
return Image.fromObject(child);
case "image-sequence":
return ImageSequence.fromObject(child);
case "path":
return Path.fromObject(child);
case "points":
return Points.fromObject(child);
case "polygon":
return Polygon.fromObject(child);
case "rectangle":
return Rectangle.fromObject(child);
case "rounded-rectangle":
return RoundedRectangle.fromObject(child);
case "shape":
return Shape.fromObject(child);
case "sprite":
return Sprite.fromObject(child);
case "star":
return Star.fromObject(child);
case "text":
return Text.fromObject(child);
}
}
return child;
}
}
static IsVisible(element, visibleOnly) {
if (!visibleOnly) {
return true;
}
let current = element;
while (current) {
if (typeof current.visible === "boolean" && !current.visible) {
return false;
}
if (typeof current.opacity === "number" && current.opacity <= 0) {
return false;
}
current = current.parent;
}
return true;
}
static VisitForHitTest(group, context, includeGroups, filter, hitOptions, tolerance, stopOnFirst) {
const children = group && group.children;
if (!children) {
return false;
}
const results = context.results;
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (!child) {
continue;
}
if (!_Group.IsVisible(child, context.visibleOnly)) {
continue;
}
const rect = typeof child.getBoundingClientRect === "function" ? child.getBoundingClientRect() : null;
if (rect && !boundsContains(rect, context.x, context.y, tolerance)) {
continue;
}
if (child instanceof _Group) {
if (includeGroups && (!filter || filter(child)) && typeof child.contains === "function" && child.contains(context.x, context.y, hitOptions)) {
results.push(child);
if (stopOnFirst) {
return true;
}
}
if (_Group.VisitForHitTest(
child,
context,
includeGroups,
filter,
hitOptions,
tolerance,
stopOnFirst
)) {
return true;
}
continue;
}
if (!(child instanceof Shape)) {
continue;
}
if (filter && !filter(child)) {
continue;
}
if (typeof child.contains !== "function") {
continue;
}
if (child.contains(context.x, context.y, hitOptions)) {
results.push(child);
if (stopOnFirst) {
return true;
}
}
}
return false;
}
/**
* @name Two.Group#copy
* @function
* @param {Two.Group} [group] - The reference {@link Two.Group}
* @returns {Two.Group}
* @description Copy the properties of one {@link Two.Group} onto another.
*/
copy(group) {
super.copy.call(this, group);
console.warn(
"Two.js: attempting to copy group. Two.Group.children copying not supported."
);
for (let i = 0; i < _Group.Properties.length; i++) {
const k = _Group.Properties[i];
if (k in group) {
this[k] = group[k];
}
}
return this;
}
/**
* @name Two.Group#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Group}
* @description Create a new instance of {@link Two.Group} with the same properties of the current group.
*/
clone(parent) {
const clone = new _Group();
const children = this.children.map(function(child) {
return child.clone();
});
clone.add(children);
clone.opacity = this.opacity;
if (this.mask) {
clone.mask = this.mask;
}
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.className = this.className;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
if (parent) {
parent.add(clone);
}
return clone._update();
}
/**
* @name Two.Group#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the group.
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = "group";
result.children = [];
result.opacity = this.opacity;
result.className = this.className;
result.mask = this.mask ? this.mask.toObject() : null;
_.each(
this.children,
(child, i) => {
result.children[i] = child.toObject();
},
this
);
return result;
}
/**
* @name Two.Group#dispose
* @function
* @returns {Two.Group}
* @description Release the group's renderer resources and detach all events.
* This method recursively disposes all child objects, unbinds the children
* collection events, and preserves the renderer type for potential re-attachment
* to a new renderer.
*/
dispose() {
super.dispose();
if (this.children) {
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (typeof child.dispose === "function") {
child.dispose();
}
}
}
if (this.children && typeof this.children.unbind === "function") {
try {
this.children.unbind();
} catch (e) {
}
}
return this;
}
/**
* @name Two.Group#getShapesAtPoint
* @function
* @param {Number} x - X coordinate in world space.
* @param {Number} y - Y coordinate in world space.
* @param {Object} [options] - Hit test configuration.
* @param {Boolean} [options.visibleOnly=true] - Limit results to visible shapes.
* @param {Boolean} [options.includeGroups=false] - Include groups in the hit results.
* @param {('all'|'deepest')} [options.mode='all'] - Whether to return all intersecting shapes or only the top-most.
* @param {Boolean} [options.deepest] - Alias for `mode: 'deepest'`.
* @param {Number} [options.precision] - Segmentation precision for curved geometry.
* @param {Number} [options.tolerance=0] - Pixel tolerance applied to hit testing.
* @param {Boolean} [options.fill] - Override fill testing behaviour.
* @param {Boolean} [options.stroke] - Override stroke testing behaviour.
* @param {Function} [options.filter] - Predicate to filter shapes from the result set.
* @returns {Shape[]} Ordered list of intersecting shapes, front to back.
* @description Traverse the group hierarchy and return shapes that contain the specified point.
* @nota-bene Expects *world-space coordinates* – the same pixel-space you get from the renderer (e.g., mouse `clientX`/`clientY` adjusted for the canvas’s offset and pixel ratio).
*/
getShapesAtPoint(x, y, options) {
const opts = options || {};
const { results, hitOptions, context, single, empty } = cache.getShapesAtPoint;
results.length = 0;
const mode = opts.mode === "deepest" || opts.deepest ? "deepest" : "all";
const visibleOnly = opts.visibleOnly !== false;
const includeGroups = !!opts.includeGroups;
const filter = typeof opts.filter === "function" ? opts.filter : null;
const tolerance = typeof opts.tolerance === "number" ? opts.tolerance : 0;
if (typeof opts.precision === "number") {
hitOptions.precision = opts.precision;
} else {
delete hitOptions.precision;
}
if (typeof opts.fill !== "undefined") {
hitOptions.fill = opts.fill;
} else {
delete hitOptions.fill;
}
if (typeof opts.stroke !== "undefined") {
hitOptions.stroke = opts.stroke;
} else {
delete hitOptions.stroke;
}
hitOptions.tolerance = tolerance;
hitOptions.ignoreVisibility = !visibleOnly;
const stopOnFirst = mode === "deepest";
context.x = x;
context.y = y;
context.visibleOnly = visibleOnly;
context.results = results;
_Group.VisitForHitTest(
this,
context,
includeGroups,
filter,
hitOptions,
tolerance,
stopOnFirst
);
if (stopOnFirst) {
if (results.length > 0) {
const first = results[0];
results.length = 0;
single[0] = first;
single.length = 1;
return single;
}
empty.length = 0;
return empty;
}
const hits = results.slice();
results.length = 0;
return hits;
}
/**
* @name Two.Group#corner
* @function
* @description Orient the children of the group to the upper left-hand corner of that group.
*/
corner() {
const rect = this.getBoundingClientRect(true);
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.translation.x -= rect.left;
child.translation.y -= rect.top;
}
if (this.mask) {
this.mask.translation.x -= rect.left;
this.mask.translation.y -= rect.top;
}
return this;
}
/**
* @name Two.Group#center
* @function
* @description Orient the children of the group to the center of that group.
*/
center() {
const rect = this.getBoundingClientRect(true);
const cx = rect.left + rect.width / 2 - this.translation.x;
const cy = rect.top + rect.height / 2 - this.translation.y;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (child.isShape) {
child.translation.x -= cx;
child.translation.y -= cy;
}
}
if (this.mask) {
this.mask.translation.x -= cx;
this.mask.translation.y -= cy;
}
return this;
}
/**
* @name Two.Group#getById
* @function
* @description Recursively search for id. Returns the first element found.
* @returns {Two.Shape} - Or `null` if nothing is found.
*/
getById(id) {
let found = null;
function search(node) {
if (node.id === id) {
return node;
} else if (node.children) {
if (node.children.ids[id]) {
return node.children.ids[id];
}
for (let i = 0; i < node.children.length; i++) {
found = search(node.children[i]);
if (found) {
return found;
}
}
}
return null;
}
return search(this);
}
/**
* @name Two.Group#getByClassName
* @function
* @description Recursively search for classes. Returns an array of matching elements.
* @returns {Two.Shape[]} - Or empty array if nothing is found.
*/
getByClassName(className) {
const found = [];
function search(node) {
if (Array.prototype.indexOf.call(node.classList, className) >= 0) {
found.push(node);
}
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
search(child);
}
}
return found;
}
return search(this);
}
/**
* @name Two.Group#getByType
* @function
* @description Recursively search for children of a specific type, e.g. {@link Two.Path}. Pass a reference to this type as the param. Returns an array of matching elements.
* @returns {Two.Shape[]} - Empty array if nothing is found.
*/
getByType(type) {
const found = [];
function search(node) {
if (node instanceof type) {
found.push(node);
}
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
search(child);
}
}
return found;
}
return search(this);
}
/**
* @name Two.Group#add
* @function
* @param {Two.Shape[]|...Two.Shape} objects - An array of objects to be added. Can also be supplied as individual arguments.
* @description Add objects to the group.
*/
add(objects) {
if (!(objects instanceof Array)) {
objects = Array.prototype.slice.call(arguments);
} else {
objects = objects.slice();
}
for (let i = 0; i < objects.length; i++) {
const child = objects[i];
if (!(child && child.id)) {
continue;
}
const index = Array.prototype.indexOf.call(this.children, child);
if (index >= 0) {
this.children.splice(index, 1);
}
this.children.push(child);
}
return this;
}
/**
* @name Two.Group#remove
* @function
* @param {Two.Shape[]|...Two.Shape} [objects=self] - An array of objects to be removed. Can be also removed as individual arguments. If no arguments are passed, then it removes itself from its parent.
* @description Remove objects from the group.
*/
remove(objects) {
const l = arguments.length, grandparent = this.parent;
if (l <= 0 && grandparent) {
grandparent.remove(this);
return this;
}
if (!(objects instanceof Array)) {
objects = Array.prototype.slice.call(arguments);
} else {
objects = objects.slice();
}
for (let i = 0; i < objects.length; i++) {
const object = objects[i];
if (!object || !this.children.ids[object.id]) {
continue;
}
const index = this.children.indexOf(object);
if (index >= 0) {
this.children.splice(index, 1);
}
}
return this;
}
/**
* @name Two.Group#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the group.
*/
getBoundingClientRect(shallow) {
let rect, matrix, tc, lc, rc, bc;
this._update(true);
let left = Infinity, right = -Infinity, top = Infinity, bottom = -Infinity;
const regex3 = /texture|gradient/i;
matrix = shallow ? this.matrix : this.worldMatrix;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (!child.visible || regex3.test(child._renderer.type)) {
continue;
}
rect = child.getBoundingClientRect(shallow);
tc = typeof rect.top !== "number" || _.isNaN(rect.top) || !isFinite(rect.top);
lc = typeof rect.left !== "number" || _.isNaN(rect.left) || !isFinite(rect.left);
rc = typeof rect.right !== "number" || _.isNaN(rect.right) || !isFinite(rect.right);
bc = typeof rect.bottom !== "number" || _.isNaN(rect.bottom) || !isFinite(rect.bottom);
if (tc || lc || rc || bc) {
continue;
}
if (shallow) {
const [ax, ay] = matrix.multiply(rect.left, rect.top);
const [bx, by] = matrix.multiply(rect.right, rect.top);
const [cx, cy] = matrix.multiply(rect.left, rect.bottom);
const [dx, dy] = matrix.multiply(rect.right, rect.bottom);
top = min3(ay, by, cy, dy, top);
left = min3(ax, bx, cx, dx, left);
right = max3(ax, bx, cx, dx, right);
bottom = max3(ay, by, cy, dy, bottom);
} else {
top = min3(rect.top, top);
left = min3(rect.left, left);
right = max3(rect.right, right);
bottom = max3(rect.bottom, bottom);
}
}
return {
top,
left,
right,
bottom,
width: right - left,
height: bottom - top
};
}
/**
* @name Two.Group#noFill
* @function
* @description Apply `noFill` method to all child shapes.
*/
noFill() {
this.children.forEach(function(child) {
child.noFill();
});
return this;
}
/**
* @name Two.Group#noStroke
* @function
* @description Apply `noStroke` method to all child shapes.
*/
noStroke() {
this.children.forEach(function(child) {
child.noStroke();
});
return this;
}
/**
* @name Two.Group#subdivide
* @function
* @description Apply `subdivide` method to all child shapes.
*/
subdivide() {
const args = arguments;
this.children.forEach(function(child) {
child.subdivide.apply(child, args);
});
return this;
}
/**
* @name Two.Group#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
let i, l, child;
if (this._flagBeginning || this._flagEnding) {
const beginning = Math.min(this._beginning, this._ending);
const ending = Math.max(this._beginning, this._ending);
const length = this.length;
let sum = 0;
const bd = beginning * length;
const ed = ending * length;
for (i = 0; i < this.children.length; i++) {
child = this.children[i];
l = child.length;
if (bd > sum + l) {
child.beginning = 1;
child.ending = 1;
} else if (ed < sum) {
child.beginning = 0;
child.ending = 0;
} else if (bd > sum && bd < sum + l) {
child.beginning = (bd - sum) / l;
child.ending = 1;
} else if (ed > sum && ed < sum + l) {
child.beginning = 0;
child.ending = (ed - sum) / l;
} else {
child.beginning = 0;
child.ending = 1;
}
sum += l;
}
}
return super._update.apply(this, arguments);
}
/**
* @name Two.Group#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
if (this._flagAdditions) {
this.additions.length = 0;
this._flagAdditions = false;
}
if (this._flagSubtractions) {
this.subtractions.length = 0;
this._flagSubtractions = false;
}
this._flagOrder = this._flagMask = this._flagOpacity = this._flagBeginning = this._flagEnding = false;
super.flagReset.call(this);
return this;
}
};
__publicField(_Group, "Children", Children);
/**
* @name Two.Group.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Group}.
*/
__publicField(_Group, "Properties", [
"fill",
"stroke",
"linewidth",
"cap",
"join",
"miter",
"closed",
"curved",
"automatic"
]);
var Group = _Group;
var proto22 = {
visible: {
enumerable: true,
get: function() {
return this._visible;
},
set: function(v) {
this._flagVisible = this._visible !== v || this._flagVisible;
this._visible = v;
}
},
opacity: {
enumerable: true,
get: function() {
return this._opacity;
},
set: function(v) {
this._flagOpacity = this._opacity !== v || this._flagOpacity;
this._opacity = v;
}
},
beginning: {
enumerable: true,
get: function() {
return this._beginning;
},
set: function(v) {
this._flagBeginning = this._beginning !== v || this._flagBeginning;
this._beginning = v;
}
},
ending: {
enumerable: true,
get: function() {
return this._ending;
},
set: function(v) {
this._flagEnding = this._ending !== v || this._flagEnding;
this._ending = v;
}
},
length: {
enumerable: true,
get: function() {
if (this._flagLength || this._length <= 0) {
this._length = 0;
if (!this.children) {
return this._length;
}
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
this._length += child.length;
}
}
return this._length;
}
},
fill: {
enumerable: true,
get: function() {
return this._fill;
},
set: function(v) {
this._fill = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.fill = v;
}
}
},
stroke: {
enumerable: true,
get: function() {
return this._stroke;
},
set: function(v) {
this._stroke = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.stroke = v;
}
}
},
linewidth: {
enumerable: true,
get: function() {
return this._linewidth;
},
set: function(v) {
this._linewidth = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.linewidth = v;
}
}
},
join: {
enumerable: true,
get: function() {
return this._join;
},
set: function(v) {
this._join = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.join = v;
}
}
},
miter: {
enumerable: true,
get: function() {
return this._miter;
},
set: function(v) {
this._miter = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.miter = v;
}
}
},
cap: {
enumerable: true,
get: function() {
return this._cap;
},
set: function(v) {
this._cap = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.cap = v;
}
}
},
closed: {
enumerable: true,
get: function() {
return this._closed;
},
set: function(v) {
this._closed = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.closed = v;
}
}
},
curved: {
enumerable: true,
get: function() {
return this._curved;
},
set: function(v) {
this._curved = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.curved = v;
}
}
},
automatic: {
enumerable: true,
get: function() {
return this._automatic;
},
set: function(v) {
this._automatic = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.automatic = v;
}
}
},
children: {
enumerable: true,
get: function() {
return this._children;
},
set: function(children) {
const insertChildren = Group.InsertChildren.bind(this);
const removeChildren = Group.RemoveChildren.bind(this);
const orderChildren = Group.OrderChildren.bind(this);
if (this._children) {
this._children.unbind();
if (this._children.length > 0) {
removeChildren(this._children);
}
}
this._children = new Children(children);
this._children.bind(Events.Types.insert, insertChildren);
this._children.bind(Events.Types.remove, removeChildren);
this._children.bind(Events.Types.order, orderChildren);
if (children.length > 0) {
insertChildren(children);
}
}
},
mask: {
enumerable: true,
get: function() {
return this._mask;
},
set: function(v) {
this._mask = v;
this._flagMask = true;
if (_.isObject(v) && !v.clip) {
v.clip = true;
}
}
},
/**
* @name Two.Group#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space for all child shapes.
* @description When `strokeAttenuation` is `false`, this property is applied to all child shapes, making their stroke widths automatically adjust to compensate for the group's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke widths scale normally with transformations.
*/
strokeAttenuation: {
enumerable: true,
get: function() {
return this._strokeAttenuation;
},
set: function(v) {
this._strokeAttenuation = !!v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (child.strokeAttenuation !== void 0) {
child.strokeAttenuation = v;
}
}
}
}
};
function replaceParent(child, newParent) {
const parent = child.parent;
let index;
if (parent === newParent) {
add();
return;
}
if (parent && parent.children.ids[child.id]) {
index = Array.prototype.indexOf.call(parent.children, child);
parent.children.splice(index, 1);
splice();
}
if (newParent) {
add();
return;
}
splice();
if (parent._flagAdditions && parent.additions.length === 0) {
parent._flagAdditions = false;
}
if (parent._flagSubtractions && parent.subtractions.length === 0) {
parent._flagSubtractions = false;
}
delete child.parent;
function add() {
if (newParent.subtractions.length > 0) {
index = Array.prototype.indexOf.call(newParent.subtractions, child);
if (index >= 0) {
newParent.subtractions.splice(index, 1);
}
}
if (newParent.additions.length > 0) {
index = Array.prototype.indexOf.call(newParent.additions, child);
if (index >= 0) {
newParent.additions.splice(index, 1);
}
}
child.parent = newParent;
newParent.additions.push(child);
newParent._flagAdditions = true;
}
function splice() {
index = Array.prototype.indexOf.call(parent.additions, child);
if (index >= 0) {
parent.additions.splice(index, 1);
}
index = Array.prototype.indexOf.call(parent.subtractions, child);
if (index < 0) {
parent.subtractions.push(child);
parent._flagSubtractions = true;
}
}
}
// src/shapes/line.js
var Line = class extends Path {
constructor(x1, y1, x2, y2) {
const points = [new Anchor(x1, y1), new Anchor(x2, y2)];
super(points);
for (let prop in proto23) {
Object.defineProperty(this, prop, proto23[prop]);
}
this.vertices[0].command = Commands.move;
this.vertices[1].command = Commands.line;
this.automatic = false;
}
};
__publicField(Line, "Properties", ["left", "right"]);
var proto23 = {
left: {
enumerable: true,
get: function() {
return this.vertices[0];
},
set: function(v) {
if (_.isObject(v)) {
this.vertices.splice(0, 1, v);
this.vertices[0].command = Commands.move;
} else {
const error = new TwoError("Two.Line.left argument is not an object.");
console.warn(error.name, error.message);
}
}
},
right: {
enumerable: true,
get: function() {
return this.vertices[1];
},
set: function(v) {
if (_.isObject(v)) {
this.vertices.splice(1, 1, v);
this.vertices[1].command = Commands.line;
} else {
const error = new TwoError("Two.Line.right argument is not an object.");
console.warn(error.name, error.message);
}
}
}
};
// src/utils/interpret-svg.js
var regex2 = {
path: /[+-]?(?:\d*\.\d+|\d+)(?:[eE][+-]\d+)?/g,
cssBackgroundImage: /url\(['"]?#([\w\d-_]*)['"]?\)/i,
unitSuffix: /[a-zA-Z%]*/i
};
var alignments = {
start: "left",
middle: "center",
end: "right"
};
var reservedAttributesToRemove = [
"id",
"class",
"transform",
"xmlns",
"viewBox"
];
var overwriteAttrs = ["x", "y", "width", "height", "href", "xlink:href"];
function getAlignment(anchor2) {
return alignments[anchor2];
}
function getBaseline(node) {
const a = node.getAttribute("dominant-baseline");
const b = node.getAttribute("alignment-baseline");
return a || b;
}
function getTagName(tag) {
return tag.replace(/svg:/gi, "").toLowerCase();
}
function applyTransformsToVector(transforms, vector3) {
vector3.x += transforms.translateX;
vector3.y += transforms.translateY;
vector3.x *= transforms.scaleX;
vector3.y *= transforms.scaleY;
if (transforms.rotation !== 0) {
const l = vector3.length();
vector3.x = l * Math.cos(transforms.rotation);
vector3.y = l * Math.sin(transforms.rotation);
}
}
function extractCSSText(text, styles) {
if (!styles) {
styles = {};
}
const commands = text.split(";");
for (let i = 0; i < commands.length; i++) {
const command = commands[i].split(":");
const name = command[0];
const value = command[1];
if (typeof name === "undefined" || typeof value === "undefined") {
continue;
}
const trimmedName = name.replace(/\s/g, "");
const trimmedValue = value.replace(/\s/g, "");
styles[trimmedName] = trimmedValue;
}
return styles;
}
function getSvgStyles(node) {
const styles = {};
const attributes = getSvgAttributes(node);
const length = Math.max(attributes.length, node.style.length);
for (let i = 0; i < length; i++) {
const command = node.style[i];
const attribute = attributes[i];
if (command) {
styles[command] = node.style[command];
}
if (attribute) {
styles[attribute] = node.getAttribute(attribute);
}
}
return styles;
}
function getSvgAttributes(node) {
const attributes = node.getAttributeNames();
for (let i = 0; i < reservedAttributesToRemove.length; i++) {
const keyword = reservedAttributesToRemove[i];
const index = Array.prototype.indexOf.call(attributes, keyword);
if (index >= 0) {
attributes.splice(index, 1);
}
}
return attributes;
}
function applySvgViewBox(node, value) {
const elements = value.split(/[\s,]/);
const x = -parseFloat(elements[0]);
const y = -parseFloat(elements[1]);
const width = parseFloat(elements[2]);
const height = parseFloat(elements[3]);
if (x && y) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if ("translation" in child) {
child.translation.add(x, y);
} else if ("x" in child) {
child.x = x;
} else if ("y" in child) {
child.y = y;
}
}
}
const xExists = typeof node.x === "number";
const yExists = typeof node.y === "number";
const widthExists = typeof node.width === "number";
const heightExists = typeof node.height === "number";
if (xExists) {
node.translation.x += node.x;
}
if (yExists) {
node.translation.y += node.y;
}
if (widthExists || heightExists) {
node.scale = new Vector(1, 1);
}
if (widthExists) {
node.scale.x = node.width / width;
}
if (heightExists) {
node.scale.y = node.height / height;
}
node.mask = new Rectangle(0, 0, width, height);
node.mask.origin.set(-width / 2, -height / 2);
return node;
}
function applySvgAttributes(node, elem, parentStyles) {
const styles = {}, attributes = {}, extracted = {};
let i, m, key, value, prop, attr;
let transforms, x, y;
let id, scene, ref, tagName;
let ca, cb, cc, error;
if (node === null) {
return styles;
}
if (root.getComputedStyle) {
const computedStyles = root.getComputedStyle(node);
i = computedStyles.length;
while (i--) {
key = computedStyles[i];
value = computedStyles[key];
if (typeof value !== "undefined") {
styles[key] = value;
}
}
}
for (i = 0; i < node.attributes.length; i++) {
attr = node.attributes[i];
if (/style/i.test(attr.nodeName)) {
extractCSSText(attr.value, extracted);
} else {
attributes[attr.nodeName] = attr.value;
}
}
if (typeof styles.opacity !== "undefined") {
styles["stroke-opacity"] = styles.opacity;
styles["fill-opacity"] = styles.opacity;
delete styles.opacity;
}
if (parentStyles) {
_.defaults(styles, parentStyles);
}
_.extend(styles, extracted, attributes);
styles.visible = !(typeof styles.display === "undefined" && /none/i.test(styles.display)) || typeof styles.visibility === "undefined" && /hidden/i.test(styles.visibility);
for (key in styles) {
value = styles[key];
switch (key) {
case "gradientTransform":
if (/none/i.test(value)) break;
m = node.gradientTransform && node.gradientTransform.baseVal && node.gradientTransform.baseVal.length > 0 ? node.gradientTransform.baseVal[0].matrix : node.getCTM ? node.getCTM() : null;
if (m === null) break;
transforms = decomposeMatrix(m);
switch (elem._renderer.type) {
case "linear-gradient":
applyTransformsToVector(transforms, elem.left);
applyTransformsToVector(transforms, elem.right);
break;
case "radial-gradient":
elem.center.x += transforms.translateX;
elem.center.y += transforms.translateY;
elem.focal.x += transforms.translateX;
elem.focal.y += transforms.translateY;
elem.radius *= Math.max(transforms.scaleX, transforms.scaleY);
break;
}
break;
case "transform":
if (/none/i.test(value)) break;
m = node.transform && node.transform.baseVal && node.transform.baseVal.length > 0 ? node.transform.baseVal[0].matrix : node.getCTM ? node.getCTM() : null;
if (m === null) break;
if (Constants.AutoCalculateImportedMatrices) {
transforms = decomposeMatrix(m);
elem.translation.set(transforms.translateX, transforms.translateY);
elem.rotation = Math.PI * (transforms.rotation / 180);
elem.scale = new Vector(transforms.scaleX, transforms.scaleY);
x = parseFloat((styles.x + "").replace("px"));
y = parseFloat((styles.y + "").replace("px"));
if (x) {
elem.translation.x = x;
}
if (y) {
elem.translation.y = y;
}
} else {
m = node.getCTM();
elem._matrix.manual = true;
elem._matrix.set(m.a, m.b, m.c, m.d, m.e, m.f);
}
break;
case "visible":
if (elem instanceof Group) {
elem._visible = value;
break;
}
elem.visible = value;
break;
case "stroke-linecap":
if (elem instanceof Group) {
elem._cap = value;
break;
}
elem.cap = value;
break;
case "stroke-linejoin":
if (elem instanceof Group) {
elem._join = value;
break;
}
elem.join = value;
break;
case "stroke-miterlimit":
if (elem instanceof Group) {
elem._miter = value;
break;
}
elem.miter = value;
break;
case "stroke-width":
if (elem instanceof Group) {
elem._linewidth = parseFloat(value);
break;
}
elem.linewidth = parseFloat(value);
break;
case "opacity":
case "stroke-opacity":
case "fill-opacity":
if (elem instanceof Group) {
elem._opacity = parseFloat(value);
break;
}
elem.opacity = parseFloat(value);
break;
case "clip-path":
if (regex2.cssBackgroundImage.test(value)) {
id = value.replace(regex2.cssBackgroundImage, "$1");
if (read.defs.current && read.defs.current.contains(id)) {
ref = read.defs.current.get(id);
if (ref && ref.childNodes.length > 0) {
ref = ref.childNodes[0];
tagName = getTagName(ref.nodeName);
elem.mask = read[tagName].call(this, ref, {});
switch (elem._renderer.type) {
case "text":
case "path":
elem.position.add(elem.mask.position);
elem.mask.position.clear();
break;
}
}
}
}
break;
case "fill":
case "stroke":
prop = (elem instanceof Group ? "_" : "") + key;
if (regex2.cssBackgroundImage.test(value)) {
id = value.replace(regex2.cssBackgroundImage, "$1");
if (read.defs.current && read.defs.current.contains(id)) {
ref = read.defs.current.get(id);
if (!ref.object) {
tagName = getTagName(ref.nodeName);
ref.object = read[tagName].call(this, ref, {});
}
ref = ref.object;
} else {
scene = getScene(this);
ref = scene.getById(id);
}
elem[prop] = ref;
} else {
elem[prop] = value;
}
break;
case "id":
elem.id = value;
break;
case "class":
case "className":
elem.classList = value.split(" ");
elem._flagClassName = true;
break;
case "x":
case "y":
ca = elem instanceof Gradient;
cb = elem instanceof LinearGradient;
cc = elem instanceof RadialGradient;
if (ca || cb || cc) {
break;
}
if (value.match("[a-z%]$") && !value.endsWith("px")) {
error = new TwoError(
"only pixel values are supported with the " + key + " attribute."
);
console.warn(error.name, error.message);
}
elem.translation[key] = parseFloat(value);
break;
case "font-family":
if (elem instanceof Text) {
elem.family = value;
}
break;
case "font-size":
if (elem instanceof Text) {
if (value.match("[a-z%]$") && !value.endsWith("px")) {
error = new TwoError(
"only pixel values are supported with the " + key + " attribute."
);
console.warn(error.name, error.message);
}
elem.size = parseFloat(value);
}
break;
case "font-weight":
if (elem instanceof Text) {
elem.weight = value;
}
break;
case "font-style":
if (elem instanceof Text) {
elem.style = value;
}
break;
case "text-decoration":
if (elem instanceof Text) {
elem.decoration = value;
}
break;
case "line-height":
if (elem instanceof Text) {
elem.leading = value;
}
break;
}
}
if (Object.keys(node.dataset).length) elem.dataset = node.dataset;
return styles;
}
function updateDefsCache(node, defsCache) {
for (let i = 0, l = node.childNodes.length; i < l; i++) {
const n = node.childNodes[i];
if (!n.id) continue;
const tagName = getTagName(node.nodeName);
if (tagName === "#text") continue;
defsCache.add(n.id, n);
}
}
function getScene(node) {
while (node.parent) {
node = node.parent;
}
return node.scene;
}
var read = {
svg: function(node) {
const defs = read.defs.current = new Registry();
const elements = node.getElementsByTagName("defs");
for (let i = 0; i < elements.length; i++) {
updateDefsCache(elements[i], defs);
}
const svg2 = read.g.call(this, node);
const viewBox = node.getAttribute("viewBox");
const x = node.getAttribute("x");
const y = node.getAttribute("y");
const width = node.getAttribute("width");
const height = node.getAttribute("height");
svg2.defs = defs;
const viewBoxExists = viewBox !== null;
const xExists = x !== null;
const yExists = y !== null;
const widthExists = width !== null;
const heightExists = height !== null;
if (xExists) {
svg2.x = parseFloat(x.replace(regex2.unitSuffix, ""));
}
if (yExists) {
svg2.y = parseFloat(y.replace(regex2.unitSuffix, ""));
}
if (widthExists) {
svg2.width = parseFloat(width.replace(regex2.unitSuffix, ""));
}
if (heightExists) {
svg2.height = parseFloat(height.replace(regex2.unitSuffix, ""));
}
if (viewBoxExists) {
applySvgViewBox(svg2, viewBox);
}
delete read.defs.current;
return svg2;
},
defs: function(node) {
return null;
},
use: function(node, styles) {
let error;
const href = node.getAttribute("href") || node.getAttribute("xlink:href");
if (!href) {
error = new TwoError("encountered with no href.");
console.warn(error.name, error.message);
return null;
}
const id = href.slice(1);
if (!read.defs.current.contains(id)) {
error = new TwoError(
"unable to find element for reference " + href + "."
);
console.warn(error.name, error.message);
return null;
}
const template = read.defs.current.get(id);
const fullNode = template.cloneNode(true);
for (let i = 0; i < node.attributes.length; i++) {
const attr = node.attributes[i];
const ca = overwriteAttrs.includes(attr.nodeName);
const cb = !fullNode.hasAttribute(attr.nodeName);
if (ca || cb) {
fullNode.setAttribute(attr.nodeName, attr.value);
}
}
const tagName = getTagName(fullNode.nodeName);
return read[tagName].call(this, fullNode, styles);
},
g: function(node, parentStyles) {
const group = new Group();
applySvgAttributes.call(this, node, group, parentStyles);
this.add(group);
const styles = getSvgStyles.call(this, node);
for (let i = 0, l = node.childNodes.length; i < l; i++) {
const n = node.childNodes[i];
const tag = n.nodeName;
if (!tag) return;
const tagName = getTagName(tag);
if (tagName in read) {
const o = read[tagName].call(group, n, styles);
if (!!o && !o.parent) {
group.add(o);
}
}
}
return group;
},
polygon: function(node, parentStyles) {
let points;
if (typeof node === "string") {
points = node;
} else {
points = node.getAttribute("points");
}
const verts = [];
points.replace(
/(-?[\d.eE-]+)[,|\s](-?[\d.eE-]+)/g,
function(match, p1, p2) {
verts.push(new Anchor(parseFloat(p1), parseFloat(p2)));
}
);
const poly = new Path(verts, true);
poly.stroke = "none";
poly.fill = "black";
applySvgAttributes.call(this, node, poly, parentStyles);
return poly;
},
polyline: function(node, parentStyles) {
const poly = read.polygon.call(this, node, parentStyles);
poly.closed = false;
return poly;
},
path: function(node, parentStyles) {
let path;
if (typeof node === "string") {
path = node;
node = null;
} else {
path = node.getAttribute("d");
}
let points = [];
let closed2 = false, relative = false;
if (path) {
let coord = new Anchor();
let control, coords;
let commands = path.match(/[a-df-z][^a-df-z]*/gi);
const last = commands.length - 1;
_.each(commands.slice(0), function(command, i) {
const items = command.slice(1).trim().match(regex2.path);
const type = command[0];
const lower = type.toLowerCase();
let bin, j, l, ct, times;
const result = [];
if (i === 0) {
commands = [];
}
switch (lower) {
case "h":
case "v":
if (items.length > 1) {
bin = 1;
}
break;
case "m":
case "l":
case "t":
if (items.length > 2) {
bin = 2;
}
break;
case "s":
case "q":
if (items.length > 4) {
bin = 4;
}
break;
case "c":
if (items.length > 6) {
bin = 6;
}
break;
case "a":
if (items.length > 7) {
bin = 7;
}
break;
}
if (bin) {
for (j = 0, l = items.length, times = 0; j < l; j += bin) {
ct = type;
if (times > 0) {
switch (type) {
case "m":
ct = "l";
break;
case "M":
ct = "L";
break;
}
}
result.push(ct + items.slice(j, j + bin).join(" "));
times++;
}
commands = Array.prototype.concat.apply(commands, result);
} else {
commands.push(command);
}
});
_.each(commands, function(command, i) {
let result, x, y;
const type = command[0];
const lower = type.toLowerCase();
coords = command.slice(1).trim().match(regex2.path);
relative = type === lower;
let x1, y1, x2, y2, x3, y3, x4, y4, reflection;
let a, b;
let anchor2, rx, ry, xAxisRotation, largeArcFlag, sweepFlag;
switch (lower) {
case "z":
if (i >= last) {
closed2 = true;
} else {
x = coord.x;
y = coord.y;
result = new Anchor(
x,
y,
void 0,
void 0,
void 0,
void 0,
Commands.close
);
for (let j = points.length - 1; j >= 0; j--) {
const point = points[j];
if (/m/i.test(point.command)) {
coord = point;
break;
}
}
}
break;
case "m":
case "l":
control = void 0;
x = parseFloat(coords[0]);
y = parseFloat(coords[1]);
result = new Anchor(
x,
y,
void 0,
void 0,
void 0,
void 0,
/m/i.test(lower) ? Commands.move : Commands.line
);
if (relative) {
result.addSelf(coord);
}
coord = result;
break;
case "h":
case "v":
a = /h/i.test(lower) ? "x" : "y";
b = /x/i.test(a) ? "y" : "x";
result = new Anchor(
void 0,
void 0,
void 0,
void 0,
void 0,
void 0,
Commands.line
);
result[a] = parseFloat(coords[0]);
result[b] = coord[b];
if (relative) {
result[a] += coord[a];
}
coord = result;
break;
case "c":
case "s":
x1 = coord.x;
y1 = coord.y;
if (!control) {
control = new Vector();
}
if (/c/i.test(lower)) {
x2 = parseFloat(coords[0]);
y2 = parseFloat(coords[1]);
x3 = parseFloat(coords[2]);
y3 = parseFloat(coords[3]);
x4 = parseFloat(coords[4]);
y4 = parseFloat(coords[5]);
} else {
reflection = getReflection(coord, control, relative);
x2 = reflection.x;
y2 = reflection.y;
x3 = parseFloat(coords[0]);
y3 = parseFloat(coords[1]);
x4 = parseFloat(coords[2]);
y4 = parseFloat(coords[3]);
}
if (relative) {
x2 += x1;
y2 += y1;
x3 += x1;
y3 += y1;
x4 += x1;
y4 += y1;
}
coord.controls.right.set(x2 - coord.x, y2 - coord.y);
result = new Anchor(
x4,
y4,
x3 - x4,
y3 - y4,
void 0,
void 0,
Commands.curve
);
coord = result;
control = result.controls.left;
break;
case "t":
case "q":
x1 = coord.x;
y1 = coord.y;
if (!control) {
control = new Vector();
}
if (/q/i.test(lower)) {
x2 = parseFloat(coords[0]);
y2 = parseFloat(coords[1]);
x3 = parseFloat(coords[0]);
y3 = parseFloat(coords[1]);
x4 = parseFloat(coords[2]);
y4 = parseFloat(coords[3]);
} else {
reflection = getReflection(coord, control, relative);
x2 = reflection.x;
y2 = reflection.y;
x3 = reflection.x;
y3 = reflection.y;
x4 = parseFloat(coords[0]);
y4 = parseFloat(coords[1]);
}
if (relative) {
x2 += x1;
y2 += y1;
x3 += x1;
y3 += y1;
x4 += x1;
y4 += y1;
}
coord.controls.right.set(
(x2 - coord.x) * 0.33,
(y2 - coord.y) * 0.33
);
result = new Anchor(
x4,
y4,
x3 - x4,
y3 - y4,
void 0,
void 0,
Commands.curve
);
coord = result;
control = result.controls.left;
break;
case "a":
x1 = coord.x;
y1 = coord.y;
rx = parseFloat(coords[0]);
ry = parseFloat(coords[1]);
xAxisRotation = parseFloat(coords[2]);
largeArcFlag = parseFloat(coords[3]);
sweepFlag = parseFloat(coords[4]);
x4 = parseFloat(coords[5]);
y4 = parseFloat(coords[6]);
if (relative) {
x4 += x1;
y4 += y1;
}
anchor2 = new Anchor(x4, y4);
anchor2.command = Commands.arc;
anchor2.rx = rx;
anchor2.ry = ry;
anchor2.xAxisRotation = xAxisRotation;
anchor2.largeArcFlag = largeArcFlag;
anchor2.sweepFlag = sweepFlag;
result = anchor2;
coord = anchor2;
control = void 0;
break;
}
if (result) {
if (Array.isArray(result)) {
points = points.concat(result);
} else {
points.push(result);
}
}
});
}
path = new Path(points, closed2, void 0, true);
path.stroke = "none";
path.fill = "black";
const rect = path.getBoundingClientRect(true);
rect.centroid = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
_.each(path.vertices, function(v) {
v.subSelf(rect.centroid);
});
applySvgAttributes.call(this, node, path, parentStyles);
path.translation.addSelf(rect.centroid);
return path;
},
circle: function(node, parentStyles) {
const x = parseFloat(node.getAttribute("cx"));
const y = parseFloat(node.getAttribute("cy"));
const r = parseFloat(node.getAttribute("r"));
const circle = new Circle(0, 0, r);
circle.stroke = "none";
circle.fill = "black";
applySvgAttributes.call(this, node, circle, parentStyles);
circle.translation.x = x;
circle.translation.y = y;
return circle;
},
ellipse: function(node, parentStyles) {
const x = parseFloat(node.getAttribute("cx"));
const y = parseFloat(node.getAttribute("cy"));
const width = parseFloat(node.getAttribute("rx"));
const height = parseFloat(node.getAttribute("ry"));
const ellipse = new Ellipse(0, 0, width, height);
ellipse.stroke = "none";
ellipse.fill = "black";
applySvgAttributes.call(this, node, ellipse, parentStyles);
ellipse.translation.x = x;
ellipse.translation.y = y;
return ellipse;
},
rect: function(node, parentStyles) {
const rx = parseFloat(node.getAttribute("rx"));
const ry = parseFloat(node.getAttribute("ry"));
if (!_.isNaN(rx) || !_.isNaN(ry)) {
return read["rounded-rect"](node);
}
const width = parseFloat(node.getAttribute("width"));
const height = parseFloat(node.getAttribute("height"));
const w2 = width / 2;
const h2 = height / 2;
const rect = new Rectangle(0, 0, width, height);
rect.stroke = "none";
rect.fill = "black";
applySvgAttributes.call(this, node, rect, parentStyles);
rect.translation.x += w2;
rect.translation.y += h2;
return rect;
},
"rounded-rect": function(node, parentStyles) {
const rx = parseFloat(node.getAttribute("rx")) || 0;
const ry = parseFloat(node.getAttribute("ry")) || 0;
const width = parseFloat(node.getAttribute("width"));
const height = parseFloat(node.getAttribute("height"));
const w2 = width / 2;
const h2 = height / 2;
const radius = new Vector(rx, ry);
const rect = new RoundedRectangle(0, 0, width, height, radius);
rect.stroke = "none";
rect.fill = "black";
applySvgAttributes.call(this, node, rect, parentStyles);
rect.translation.x += w2;
rect.translation.y += h2;
return rect;
},
line: function(node, parentStyles) {
const x1 = parseFloat(node.getAttribute("x1"));
const y1 = parseFloat(node.getAttribute("y1"));
const x2 = parseFloat(node.getAttribute("x2"));
const y2 = parseFloat(node.getAttribute("y2"));
const line = new Line(x1, y1, x2, y2).noFill();
applySvgAttributes.call(this, node, line, parentStyles);
return line;
},
lineargradient: function(node, parentStyles) {
let units = node.getAttribute("gradientUnits");
let spread = node.getAttribute("spreadMethod");
if (!units) {
units = "objectBoundingBox";
}
if (!spread) {
spread = "pad";
}
let x1 = parseFloat(node.getAttribute("x1") || 0);
let y1 = parseFloat(node.getAttribute("y1") || 0);
let x2 = parseFloat(node.getAttribute("x2") || 0);
let y2 = parseFloat(node.getAttribute("y2") || 0);
const ox = (x2 + x1) / 2;
const oy = (y2 + y1) / 2;
if (/userSpaceOnUse/i.test(units)) {
x1 -= ox;
y1 -= oy;
x2 -= ox;
y2 -= oy;
}
const stops = [];
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
let offset = child.getAttribute("offset");
if (/%/gi.test(offset)) {
offset = parseFloat(offset.replace(/%/gi, "")) / 100;
}
offset = parseFloat(offset);
let color = child.getAttribute("stop-color");
let opacity = child.getAttribute("stop-opacity");
let style = child.getAttribute("style");
let matches;
if (color === null) {
matches = style ? style.match(/stop-color:\s?([#a-fA-F0-9]*)/) : false;
color = matches && matches.length > 1 ? matches[1] : void 0;
}
if (opacity === null) {
matches = style ? style.match(/stop-opacity:\s?([0-9.-]*)/) : false;
opacity = matches && matches.length > 1 ? parseFloat(matches[1]) : 1;
} else {
opacity = parseFloat(opacity);
}
stops.push(new Stop(offset, color, opacity));
}
const gradient = new LinearGradient(x1, y1, x2, y2, stops);
gradient.spread = spread;
gradient.units = units;
applySvgAttributes.call(this, node, gradient, parentStyles);
return gradient;
},
radialgradient: function(node, parentStyles) {
let units = node.getAttribute("gradientUnits");
let spread = node.getAttribute("spreadMethod");
if (!units) {
units = "objectBoundingBox";
}
if (!spread) {
spread = "pad";
}
let cx = parseFloat(node.getAttribute("cx")) || 0;
let cy = parseFloat(node.getAttribute("cy")) || 0;
let r = parseFloat(node.getAttribute("r"));
let fx = parseFloat(node.getAttribute("fx"));
let fy = parseFloat(node.getAttribute("fy"));
if (_.isNaN(fx)) {
fx = cx;
}
if (_.isNaN(fy)) {
fy = cy;
}
const ox = Math.abs(cx + fx) / 2;
const oy = Math.abs(cy + fy) / 2;
if (/userSpaceOnUse/i.test(units)) {
cx -= ox;
cy -= oy;
fx -= ox;
fy -= oy;
}
const stops = [];
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
let offset = child.getAttribute("offset");
if (/%/gi.test(offset)) {
offset = parseFloat(offset.replace(/%/gi, "")) / 100;
}
offset = parseFloat(offset);
let color = child.getAttribute("stop-color");
let opacity = child.getAttribute("stop-opacity");
let style = child.getAttribute("style");
let matches;
if (color === null) {
matches = style ? style.match(/stop-color:\s?([#a-fA-F0-9]*)/) : false;
color = matches && matches.length > 1 ? matches[1] : void 0;
}
if (opacity === null) {
matches = style ? style.match(/stop-opacity:\s?([0-9.-]*)/) : false;
opacity = matches && matches.length > 1 ? parseFloat(matches[1]) : 1;
} else {
opacity = parseFloat(opacity);
}
stops.push(new Stop(offset, color, opacity));
}
const gradient = new RadialGradient(cx, cy, r, stops, fx, fy);
gradient.spread = spread;
gradient.units = units;
applySvgAttributes.call(this, node, gradient, parentStyles);
return gradient;
},
text: function(node, parentStyles) {
const alignment = getAlignment(node.getAttribute("text-anchor")) || "left";
const baseline = getBaseline(node) || "baseline";
let message = "";
if (node.childNodes.length > 0 && node.childNodes[0].tagName === "TSPAN") {
message = node.childNodes[0].textContent;
} else {
message = node.textContent;
}
const text = new Text(message);
applySvgAttributes.call(this, node, text, parentStyles);
text.alignment = alignment;
text.baseline = baseline;
return text;
},
clippath: function(node, parentStyles) {
if (read.defs.current && !read.defs.current.contains(node.id)) {
read.defs.current.add(node.id, node);
}
return null;
},
image: function(node, parentStyles) {
let error;
const href = node.getAttribute("href") || node.getAttribute("xlink:href");
if (!href) {
error = new TwoError("encountered with no href.");
console.warn(error.name, error.message);
return null;
}
const x = parseFloat(node.getAttribute("x")) || 0;
const y = parseFloat(node.getAttribute("y")) || 0;
const width = parseFloat(node.getAttribute("width"));
const height = parseFloat(node.getAttribute("height"));
const sprite = new Sprite(href, x, y);
if (!_.isNaN(width)) {
sprite.width = width;
}
if (!_.isNaN(height)) {
sprite.height = height;
}
applySvgAttributes.call(this, node, sprite, parentStyles);
return sprite;
}
};
// src/utils/xhr.js
function xhr(path, callback) {
const xhr2 = new XMLHttpRequest();
xhr2.open("GET", path);
xhr2.onreadystatechange = function() {
if (xhr2.readyState === 4 && xhr2.status === 200) {
callback(xhr2.responseText);
}
};
xhr2.send();
return xhr2;
}
// src/effects/image.js
var _Image = class _Image extends Rectangle {
constructor(src, ox, oy, width, height, mode) {
super(ox, oy, width || 1, height || 1);
/**
* @name Two.Image#_flagTexture
* @private
* @property {Boolean} - Determines whether the {@link Two.Image#texture} needs updating.
*/
__publicField(this, "_flagTexture", false);
/**
* @name Two.Image#_flagMode
* @private
* @property {Boolean} - Determines whether the {@link Two.Image#mode} needs updating.
*/
__publicField(this, "_flagMode", false);
/**
* @name Two.Image#_texture
* @private
* @see {@link Two.Image#texture}
*/
__publicField(this, "_texture", null);
/**
* @name Two.Image#_mode
* @private
* @see {@link Two.Image#mode}
*/
__publicField(this, "_mode", "fill");
this._renderer.type = "image";
for (let prop in proto24) {
Object.defineProperty(this, prop, proto24[prop]);
}
this.noStroke();
this.noFill();
if (src instanceof Texture) {
this.texture = src;
} else if (typeof src === "string") {
this.texture = new Texture(src);
}
if (typeof mode === "string") {
this.mode = mode;
}
this._update();
}
/**
* @name Two.Image.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Image} to create a new instance
* @returns {Two.Image}
* @description Create a new {@link Two.Image} from an object notation of a {@link Two.Image}.
* @nota-bene Works in conjunction with {@link Two.Image#toObject}
*/
static fromObject(obj) {
const image = new _Image().copy(obj);
if ("id" in obj) {
image.id = obj.id;
}
return image;
}
/**
* @name Two.Image#copy
* @function
* @param {Two.Image} image - The reference {@link Two.Image}
* @description Copy the properties of one {@link Two.Image} onto another.
*/
copy(image) {
super.copy.call(this, image);
for (let i = 0; i < _Image.Properties.length; i++) {
const k = _Image.Properties[i];
if (k in image) {
this[k] = image[k];
}
}
return this;
}
/**
* @name Two.Image#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Image}
* @description Create a new instance of {@link Two.Image} with the same properties of the current image.
*/
clone(parent) {
const clone = new _Image(
this.texture,
this.translation.x,
this.translation.y,
this.width,
this.height
);
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Image#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the image.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = "image";
object.texture = this.texture.toObject();
object.mode = this.mode;
return object;
}
/**
* @name Two.Image#dispose
* @function
* @returns {Two.Image}
* @description Release the image's renderer resources and detach all events.
* This method disposes the texture (calling dispose() for thorough cleanup) and inherits comprehensive
* cleanup from the Rectangle/Path hierarchy while preserving the renderer type
* for potential re-attachment.
*/
dispose() {
super.dispose();
if (this._texture && typeof this._texture.dispose === "function") {
this._texture.dispose();
} else if (this._texture && typeof this._texture.unbind === "function") {
this._texture.unbind();
}
return this;
}
/**
* @name Two.Image#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
const effect = this._texture;
if (effect) {
if (this._flagTexture) {
this.fill = effect;
}
if (effect.loaded) {
const iw = effect.image.width;
const ih = effect.image.height;
const rw = this.width;
const rh = this.height;
const scaleX = rw / iw;
const scaleY = rh / ih;
switch (this._mode) {
case _Image.Modes.fill: {
const scale = Math.max(scaleX, scaleY);
effect.scale = scale;
effect.offset.x = 0;
effect.offset.y = 0;
effect.repeat = "repeat";
break;
}
case _Image.Modes.fit: {
const scale = Math.min(scaleX, scaleY);
effect.scale = scale;
effect.offset.x = 0;
effect.offset.y = 0;
effect.repeat = "no-repeat";
break;
}
case _Image.Modes.crop: {
break;
}
case _Image.Modes.tile: {
effect.offset.x = (iw - rw) / 2;
effect.offset.y = (ih - rh) / 2;
effect.repeat = "repeat";
break;
}
case _Image.Modes.stretch:
default: {
effect.scale = new Vector(scaleX, scaleY);
effect.offset.x = 0;
effect.offset.y = 0;
effect.repeat = "repeat";
}
}
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Image#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
super.flagReset.call(this);
this._flagTexture = this._flagMode = false;
return this;
}
};
/**
* @name Two.Image.Modes
* @property {Object} Modes - Different mode types to render an image inspired by Figma.
* @property {String} Modes.fill - Scale image to fill the bounds while preserving aspect ratio.
* @property {String} Modes.fit - Scale image to fit within bounds while preserving aspect ratio.
* @property {String} Modes.crop - Scale image to fill bounds while preserving aspect ratio, cropping excess.
* @property {String} Modes.tile - Repeat image at original size to fill the bounds.
* @property {String} Modes.stretch - Stretch image to fill dimensions, ignoring aspect ratio.
*/
__publicField(_Image, "Modes", {
fill: "fill",
fit: "fit",
crop: "crop",
tile: "tile",
stretch: "stretch"
});
/**
* @name Two.Image.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Image}.
*/
__publicField(_Image, "Properties", ["texture", "mode"]);
var Image2 = _Image;
var proto24 = {
texture: {
enumerable: true,
get: function() {
return this._texture;
},
set: function(v) {
this._texture = v;
this._flagTexture = true;
}
},
mode: {
enumerable: true,
get: function() {
return this._mode;
},
set: function(v) {
this._mode = v;
this._flagMode = true;
}
}
};
// src/renderers/canvas.js
var emptyArray = [];
var max4 = Math.max;
var min4 = Math.min;
var abs = Math.abs;
var sin6 = Math.sin;
var cos6 = Math.cos;
var acos = Math.acos;
var sqrt = Math.sqrt;
var canvas2 = {
isHidden: /(undefined|none|transparent)/i,
alignments: {
left: "start",
middle: "center",
right: "end"
},
baselines: {
top: "top",
middle: "middle",
bottom: "bottom",
baseline: "alphabetic"
},
getRendererType: function(type) {
return type in canvas2 ? type : "path";
},
group: {
renderChild: function(child) {
const prop = canvas2.getRendererType(child._renderer.type);
canvas2[prop].render.call(child, this.ctx, true, this.clip);
},
render: function(ctx) {
if (!this._visible) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const matrix = this._matrix.elements;
const parent = this.parent;
this._renderer.opacity = this._opacity * (parent && parent._renderer ? parent._renderer.opacity : 1);
const mask = this._mask;
const defaultMatrix = isDefaultMatrix(matrix);
const shouldIsolate = !defaultMatrix || !!mask;
if (!this._renderer.context) {
this._renderer.context = {};
}
this._renderer.context.ctx = ctx;
if (shouldIsolate) {
ctx.save();
if (!defaultMatrix) {
ctx.transform(
matrix[0],
matrix[3],
matrix[1],
matrix[4],
matrix[2],
matrix[5]
);
}
}
if (mask) {
const prop = canvas2.getRendererType(mask._renderer.type);
canvas2[prop].render.call(mask, ctx, true);
}
if (this._opacity > 0 && this._scale !== 0) {
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
const prop = canvas2.getRendererType(child._renderer.type);
canvas2[prop].render.call(child, ctx);
}
}
if (shouldIsolate) {
ctx.restore();
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
path: {
render: function(ctx, forced, parentClipped) {
let matrix, stroke, linewidth, fill, opacity, visible, cap, join, miter, closed2, commands, length, last, prev, a, b, c, d, ux, uy, vx, vy, ar, bl, br, cl, x, y, mask, clip, defaultMatrix, isOffset, dashes, po;
po = this.parent && this.parent._renderer ? this.parent._renderer.opacity : 1;
mask = this._mask;
clip = this._clip;
opacity = this._opacity * (po || 1);
visible = this._visible;
if (!forced && (!visible || clip || opacity === 0)) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
matrix = this._matrix.elements;
stroke = this._stroke;
linewidth = this._linewidth;
fill = this._fill;
cap = this._cap;
join = this._join;
miter = this._miter;
closed2 = this._closed;
commands = this._renderer.vertices;
length = commands.length;
last = length - 1;
defaultMatrix = isDefaultMatrix(matrix);
dashes = this.dashes;
if (!defaultMatrix) {
ctx.save();
ctx.transform(
matrix[0],
matrix[3],
matrix[1],
matrix[4],
matrix[2],
matrix[5]
);
}
if (mask) {
const prop = canvas2.getRendererType(mask._renderer.type);
canvas2[prop].render.call(mask, ctx, true);
}
if (fill) {
if (typeof fill === "string") {
ctx.fillStyle = fill;
} else {
const prop = canvas2.getRendererType(fill._renderer.type);
canvas2[prop].render.call(fill, ctx, this);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === "string") {
ctx.strokeStyle = stroke;
} else {
const prop = canvas2.getRendererType(stroke._renderer.type);
canvas2[prop].render.call(stroke, ctx, this);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(this);
}
if (miter) {
ctx.miterLimit = miter;
}
if (join) {
ctx.lineJoin = join;
}
if (!closed2 && cap) {
ctx.lineCap = cap;
}
}
if (typeof opacity === "number") {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
ctx.beginPath();
let rx, ry, xAxisRotation, largeArcFlag, sweepFlag, ax, ay;
for (let i = 0; i < length; i++) {
b = commands[i];
x = b.x;
y = b.y;
switch (b.command) {
case Commands.close:
ctx.closePath();
break;
case Commands.arc:
rx = b.rx;
ry = b.ry;
xAxisRotation = b.xAxisRotation;
largeArcFlag = b.largeArcFlag;
sweepFlag = b.sweepFlag;
prev = closed2 ? mod(i - 1, length) : max4(i - 1, 0);
a = commands[prev];
ax = a.x;
ay = a.y;
canvas2.renderSvgArcCommand(
ctx,
ax,
ay,
rx,
ry,
largeArcFlag,
sweepFlag,
xAxisRotation,
x,
y
);
break;
case Commands.curve:
prev = closed2 ? mod(i - 1, length) : Math.max(i - 1, 0);
a = commands[prev];
ar = a.controls && a.controls.right || Vector.zero;
bl = b.controls && b.controls.left || Vector.zero;
if (a._relative) {
vx = ar.x + a.x;
vy = ar.y + a.y;
} else {
vx = ar.x;
vy = ar.y;
}
if (b._relative) {
ux = bl.x + b.x;
uy = bl.y + b.y;
} else {
ux = bl.x;
uy = bl.y;
}
ctx.bezierCurveTo(vx, vy, ux, uy, x, y);
if (i >= last && closed2) {
c = d;
br = b.controls && b.controls.right || Vector.zero;
cl = c.controls && c.controls.left || Vector.zero;
if (b._relative) {
vx = br.x + b.x;
vy = br.y + b.y;
} else {
vx = br.x;
vy = br.y;
}
if (c._relative) {
ux = cl.x + c.x;
uy = cl.y + c.y;
} else {
ux = cl.x;
uy = cl.y;
}
x = c.x;
y = c.y;
ctx.bezierCurveTo(vx, vy, ux, uy, x, y);
}
break;
case Commands.line:
ctx.lineTo(x, y);
break;
case Commands.move:
d = b;
ctx.moveTo(x, y);
break;
}
}
if (closed2) {
ctx.closePath();
}
if (!clip && !parentClipped) {
if (!canvas2.isHidden.test(fill)) {
isOffset = fill._renderer && fill._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(fill._renderer.scale.x, fill._renderer.scale.y);
}
ctx.fill();
if (isOffset) {
ctx.restore();
}
}
if (!canvas2.isHidden.test(stroke)) {
isOffset = stroke._renderer && stroke._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(
-stroke._renderer.offset.x,
-stroke._renderer.offset.y
);
ctx.scale(stroke._renderer.scale.x, stroke._renderer.scale.y);
ctx.lineWidth = linewidth / stroke._renderer.scale.x;
}
ctx.stroke();
if (isOffset) {
ctx.restore();
}
}
}
if (!defaultMatrix) {
ctx.restore();
}
if (clip && !parentClipped) {
ctx.clip();
}
if (dashes && dashes.length > 0) {
ctx.setLineDash(emptyArray);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
points: {
render: function(ctx, forced, parentClipped) {
let me, stroke, linewidth, fill, opacity, visible, size, commands, length, b, x, y, defaultMatrix, isOffset, dashes, po;
po = this.parent && this.parent._renderer ? this.parent._renderer.opacity : 1;
opacity = this._opacity * (po || 1);
visible = this._visible;
if (!forced && (!visible || opacity === 0)) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
me = this._matrix.elements;
stroke = this._stroke;
linewidth = this._linewidth;
fill = this._fill;
commands = this._renderer.collection;
length = commands.length;
defaultMatrix = isDefaultMatrix(me);
dashes = this.dashes;
size = this._size;
if (!defaultMatrix) {
ctx.save();
ctx.transform(me[0], me[3], me[1], me[4], me[2], me[5]);
}
if (fill) {
if (typeof fill === "string") {
ctx.fillStyle = fill;
} else {
const prop = canvas2.getRendererType(fill._renderer.type);
canvas2[prop].render.call(fill, ctx, this);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === "string") {
ctx.strokeStyle = stroke;
} else {
const prop = canvas2.getRendererType(stroke._renderer.type);
canvas2[prop].render.call(stroke, ctx, this);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(this);
}
}
if (typeof opacity === "number") {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
ctx.beginPath();
let radius = size * 0.5, m;
if (!this._sizeAttenuation) {
m = this.worldMatrix.elements;
m = decomposeMatrix(m[0], m[3], m[1], m[4], m[2], m[5]);
radius /= Math.max(m.scaleX, m.scaleY);
}
for (let i = 0; i < length; i++) {
b = commands[i];
x = b.x;
y = b.y;
ctx.moveTo(x + radius, y);
ctx.arc(x, y, radius, 0, TWO_PI);
}
if (!parentClipped) {
if (!canvas2.isHidden.test(fill)) {
isOffset = fill._renderer && fill._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(fill._renderer.scale.x, fill._renderer.scale.y);
}
ctx.fill();
if (isOffset) {
ctx.restore();
}
}
if (!canvas2.isHidden.test(stroke)) {
isOffset = stroke._renderer && stroke._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(
-stroke._renderer.offset.x,
-stroke._renderer.offset.y
);
ctx.scale(stroke._renderer.scale.x, stroke._renderer.scale.y);
ctx.lineWidth = linewidth / stroke._renderer.scale.x;
}
ctx.stroke();
if (isOffset) {
ctx.restore();
}
}
}
if (!defaultMatrix) {
ctx.restore();
}
if (dashes && dashes.length > 0) {
ctx.setLineDash(emptyArray);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
text: {
render: function(ctx, forced, parentClipped) {
const po = this.parent && this.parent._renderer ? this.parent._renderer.opacity : 1;
const opacity = this._opacity * po;
const visible = this._visible;
const mask = this._mask;
const clip = this._clip;
if (!forced && (!visible || clip || opacity === 0)) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const matrix = this._matrix.elements;
const stroke = this._stroke;
const linewidth = this._linewidth;
const fill = this._fill;
const decoration = this._decoration;
const direction = this._direction;
const defaultMatrix = isDefaultMatrix(matrix);
const isOffset = fill._renderer && fill._renderer.offset && stroke._renderer && stroke._renderer.offset;
const dashes = this.dashes;
const alignment = canvas2.alignments[this._alignment] || this._alignment;
const baseline = canvas2.baselines[this._baseline] || this._baseline;
let a, b, c, d, e, sx, sy, x1, y1, x2, y2;
if (!defaultMatrix) {
ctx.save();
ctx.transform(
matrix[0],
matrix[3],
matrix[1],
matrix[4],
matrix[2],
matrix[5]
);
}
if (mask) {
const prop = canvas2.getRendererType(mask._renderer.type);
canvas2[prop].render.call(mask, ctx, true);
}
if (!isOffset) {
ctx.font = [
this._style,
this._weight,
this._size + "px/" + this._leading + "px",
this._family
].join(" ");
}
ctx.textAlign = alignment;
ctx.textBaseline = baseline;
ctx.direction = direction;
if (fill) {
if (typeof fill === "string") {
ctx.fillStyle = fill;
} else {
const prop = canvas2.getRendererType(fill._renderer.type);
canvas2[prop].render.call(fill, ctx, this);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === "string") {
ctx.strokeStyle = stroke;
} else {
const prop = canvas2.getRendererType(stroke._renderer.type);
canvas2[prop].render.call(stroke, ctx, this);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(this);
}
}
if (typeof opacity === "number") {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
if (!clip && !parentClipped) {
if (!canvas2.isHidden.test(fill)) {
if (fill._renderer && fill._renderer.offset) {
sx = fill._renderer.scale.x;
sy = fill._renderer.scale.y;
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(sx, sy);
a = this._size / fill._renderer.scale.y;
b = this._leading / fill._renderer.scale.y;
ctx.font = [
this._style,
this._weight,
a + "px/",
b + "px",
this._family
].join(" ");
c = fill._renderer.offset.x / fill._renderer.scale.x;
d = fill._renderer.offset.y / fill._renderer.scale.y;
ctx.fillText(this.value, c, d);
ctx.restore();
} else {
ctx.fillText(this.value, 0, 0);
}
}
if (!canvas2.isHidden.test(stroke)) {
if (stroke._renderer && stroke._renderer.offset) {
sx = stroke._renderer.scale.x;
sy = stroke._renderer.scale.y;
ctx.save();
ctx.translate(
-stroke._renderer.offset.x,
-stroke._renderer.offset.y
);
ctx.scale(sx, sy);
a = this._size / stroke._renderer.scale.y;
b = this._leading / stroke._renderer.scale.y;
ctx.font = [
this._style,
this._weight,
a + "px/",
b + "px",
this._family
].join(" ");
c = stroke._renderer.offset.x / stroke._renderer.scale.x;
d = stroke._renderer.offset.y / stroke._renderer.scale.y;
e = linewidth / stroke._renderer.scale.x;
ctx.lineWidth = e;
ctx.strokeText(this.value, c, d);
ctx.restore();
} else {
ctx.strokeText(this.value, 0, 0);
}
}
}
if (/(underline|strikethrough)/i.test(decoration)) {
const metrics = ctx.measureText(this.value);
let scalar = 1;
switch (decoration) {
case "underline":
y1 = metrics.actualBoundingBoxDescent;
y2 = metrics.actualBoundingBoxDescent;
break;
case "strikethrough":
y1 = 0;
y2 = 0;
scalar = 0.5;
break;
}
switch (baseline) {
case "top":
y1 += this._size * scalar;
y2 += this._size * scalar;
break;
case "baseline":
case "bottom":
y1 -= this._size * scalar;
y2 -= this._size * scalar;
break;
}
switch (alignment) {
case "left":
case "start":
x1 = 0;
x2 = metrics.width;
break;
case "right":
case "end":
x1 = -metrics.width;
x2 = 0;
break;
default:
x1 = -metrics.width / 2;
x2 = metrics.width / 2;
}
ctx.lineWidth = Math.max(Math.floor(this._size / 15), 1);
ctx.strokeStyle = ctx.fillStyle;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
if (!defaultMatrix) {
ctx.restore();
}
if (clip && !parentClipped) {
ctx.clip();
}
if (dashes && dashes.length > 0) {
ctx.setLineDash(emptyArray);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
"linear-gradient": {
render: function(ctx, parent) {
if (!parent) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
if (!this._renderer.effect || this._flagEndPoints || this._flagStops || this._flagUnits) {
let rect;
let lx = this.left._x;
let ly = this.left._y;
let rx = this.right._x;
let ry = this.right._y;
if (/objectBoundingBox/i.test(this._units)) {
rect = parent.getBoundingClientRect(true);
lx = (lx - 0.5) * rect.width;
ly = (ly - 0.5) * rect.height;
rx = (rx - 0.5) * rect.width;
ry = (ry - 0.5) * rect.height;
}
this._renderer.effect = ctx.createLinearGradient(lx, ly, rx, ry);
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
this._renderer.effect.addColorStop(stop._offset, stop._color);
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
"radial-gradient": {
render: function(ctx, parent) {
if (!parent) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
if (!this._renderer.effect || this._flagCenter || this._flagFocal || this._flagRadius || this._flagStops || this._flagUnits) {
let rect;
let cx = this.center._x;
let cy = this.center._y;
let fx = this.focal._x;
let fy = this.focal._y;
let radius = this._radius;
if (/objectBoundingBox/i.test(this._units)) {
rect = parent.getBoundingClientRect(true);
cx = (cx - 0.5) * rect.width * 0.5;
cy = (cy - 0.5) * rect.height * 0.5;
fx = (fx - 0.5) * rect.width * 0.5;
fy = (fy - 0.5) * rect.height * 0.5;
radius *= Math.min(rect.width, rect.height);
}
this._renderer.effect = ctx.createRadialGradient(
cx,
cy,
0,
fx,
fy,
radius
);
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
this._renderer.effect.addColorStop(stop._offset, stop._color);
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
texture: {
render: function(ctx) {
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const image = this.image;
if (!this._renderer.effect || (this._flagLoaded || this._flagImage || this._flagVideo || this._flagRepeat) && this.loaded) {
this._renderer.effect = ctx.createPattern(this.image, this._repeat);
}
if (this._flagOffset || this._flagLoaded || this._flagScale) {
if (!(this._renderer.offset instanceof Vector)) {
this._renderer.offset = new Vector();
}
this._renderer.offset.x = -this._offset.x;
this._renderer.offset.y = -this._offset.y;
if (image) {
this._renderer.offset.x += image.width / 2;
this._renderer.offset.y += image.height / 2;
if (this._scale instanceof Vector) {
this._renderer.offset.x *= this._scale.x;
this._renderer.offset.y *= this._scale.y;
} else {
this._renderer.offset.x *= this._scale;
this._renderer.offset.y *= this._scale;
}
}
}
if (this._flagScale || this._flagLoaded) {
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
if (this._scale instanceof Vector) {
this._renderer.scale.copy(this._scale);
} else {
this._renderer.scale.set(this._scale, this._scale);
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
renderSvgArcCommand: function(ctx, ax, ay, rx, ry, largeArcFlag, sweepFlag, xAxisRotation, x, y) {
xAxisRotation = xAxisRotation * Math.PI / 180;
rx = abs(rx);
ry = abs(ry);
const dx2 = (ax - x) / 2;
const dy2 = (ay - y) / 2;
const x1p = cos6(xAxisRotation) * dx2 + sin6(xAxisRotation) * dy2;
const y1p = -sin6(xAxisRotation) * dx2 + cos6(xAxisRotation) * dy2;
const x1ps = x1p * x1p;
const y1ps = y1p * y1p;
let rxs = rx * rx;
let rys = ry * ry;
const cr = x1ps / rxs + y1ps / rys;
if (cr > 1) {
const s = sqrt(cr);
rx = s * rx;
ry = s * ry;
rxs = rx * rx;
rys = ry * ry;
}
const dq = rxs * y1ps + rys * x1ps;
const pq = (rxs * rys - dq) / dq;
let q = sqrt(max4(0, pq));
if (largeArcFlag === sweepFlag) q = -q;
const cxp = q * rx * y1p / ry;
const cyp = -q * ry * x1p / rx;
const cx = cos6(xAxisRotation) * cxp - sin6(xAxisRotation) * cyp + (ax + x) / 2;
const cy = sin6(xAxisRotation) * cxp + cos6(xAxisRotation) * cyp + (ay + y) / 2;
const startAngle = svgAngle2(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
const delta = svgAngle2(
(x1p - cxp) / rx,
(y1p - cyp) / ry,
(-x1p - cxp) / rx,
(-y1p - cyp) / ry
) % TWO_PI;
const endAngle = startAngle + delta;
const clockwise = sweepFlag === 0;
renderArcEstimate(
ctx,
cx,
cy,
rx,
ry,
startAngle,
endAngle,
clockwise,
xAxisRotation
);
}
};
var Renderer = class extends Events {
constructor(params) {
super();
const smoothing = params.smoothing !== false;
this.domElement = params.domElement || document.createElement("canvas");
this.ctx = this.domElement.getContext("2d");
this.overdraw = params.overdraw || false;
if (typeof this.ctx.imageSmoothingEnabled !== "undefined") {
this.ctx.imageSmoothingEnabled = smoothing;
}
this.scene = new Group();
this.scene.parent = this;
}
/**
* @name Two.CanvasRenderer#setSize
* @function
* @fires resize
* @param {Number} width - The new width of the renderer.
* @param {Number} height - The new height of the renderer.
* @param {Number} [ratio] - The new pixel ratio (pixel density) of the renderer. Defaults to calculate the pixel density of the user's screen.
* @description Change the size of the renderer.
*/
setSize(width, height, ratio) {
this.width = width;
this.height = height;
this.ratio = typeof ratio === "undefined" ? getRatio(this.ctx) : ratio;
this.domElement.width = width * this.ratio;
this.domElement.height = height * this.ratio;
if (this.domElement.style) {
_.extend(this.domElement.style, {
width: width + "px",
height: height + "px"
});
}
return this.trigger(Events.Types.resize, width, height, ratio);
}
/**
* @name Two.CanvasRenderer#render
* @function
* @description Render the current scene to the ``.
*/
render() {
const isOne = this.ratio === 1;
if (!isOne) {
this.ctx.save();
this.ctx.scale(this.ratio, this.ratio);
}
if (!this.overdraw) {
this.ctx.clearRect(0, 0, this.width, this.height);
}
canvas2.group.render.call(this.scene, this.ctx);
if (!isOne) {
this.ctx.restore();
}
return this;
}
};
/**
* @name Two.CanvasRenderer.Utils
* @property {Object} - A massive object filled with utility functions and properties to render Two.js objects to a ``.
*/
__publicField(Renderer, "Utils", canvas2);
function renderArcEstimate(ctx, ox, oy, rx, ry, startAngle, endAngle, clockwise, xAxisRotation) {
const delta = endAngle - startAngle;
const epsilon = Curve.Tolerance.epsilon;
const samePoints = Math.abs(delta) < epsilon;
let deltaAngle = mod(delta, TWO_PI);
if (deltaAngle < epsilon) {
if (samePoints) {
deltaAngle = 0;
} else {
deltaAngle = TWO_PI;
}
}
if (clockwise === true && !samePoints) {
if (deltaAngle === TWO_PI) {
deltaAngle = -TWO_PI;
} else {
deltaAngle = deltaAngle - TWO_PI;
}
}
for (let i = 0; i < Constants.Resolution; i++) {
const t = i / (Constants.Resolution - 1);
const angle = startAngle + t * deltaAngle;
let x = ox + rx * Math.cos(angle);
let y = oy + ry * Math.sin(angle);
if (xAxisRotation !== 0) {
const cos7 = Math.cos(xAxisRotation);
const sin7 = Math.sin(xAxisRotation);
const tx = x - ox;
const ty = y - oy;
x = tx * cos7 - ty * sin7 + ox;
y = tx * sin7 + ty * cos7 + oy;
}
ctx.lineTo(x, y);
}
}
function svgAngle2(ux, uy, vx, vy) {
const dot = ux * vx + uy * vy;
const len = sqrt(ux * ux + uy * uy) * sqrt(vx * vx + vy * vy);
let ang = acos(max4(-1, min4(1, dot / len)));
if (ux * vy - uy * vx < 0) {
ang = -ang;
}
return ang;
}
function isDefaultMatrix(m) {
return m[0] === 1 && m[3] === 0 && m[1] === 0 && m[4] === 1 && m[2] === 0 && m[5] === 0;
}
// src/renderers/svg.js
var svg = {
version: 1.1,
ns: "http://www.w3.org/2000/svg",
xlink: "http://www.w3.org/1999/xlink",
alignments: {
left: "start",
center: "middle",
right: "end"
},
baselines: {
top: "hanging",
middle: "middle",
bottom: "ideographic",
baseline: "alphabetic"
},
// Create an svg namespaced element.
createElement: function(name, attrs) {
const tag = name;
const elem = document.createElementNS(svg.ns, tag);
if (tag === "svg") {
attrs = _.defaults(attrs || {}, {
version: svg.version
});
}
if (attrs && Object.keys(attrs).length > 0) {
svg.setAttributes(elem, attrs);
}
return elem;
},
// Add attributes from an svg element.
setAttributes: function(elem, attrs) {
const keys = Object.keys(attrs);
for (let i = 0; i < keys.length; i++) {
if (/href/.test(keys[i])) {
elem.setAttributeNS(svg.xlink, keys[i], attrs[keys[i]]);
} else {
elem.setAttribute(keys[i], attrs[keys[i]]);
}
}
return this;
},
// Remove attributes from an svg element.
removeAttributes: function(elem, attrs) {
for (let key in attrs) {
elem.removeAttribute(key);
}
return this;
},
// Turn a set of vertices into a string for the d property of a path
// element. It is imperative that the string collation is as fast as
// possible, because this call will be happening multiple times a
// second.
toString: function(points, closed2) {
let l = points.length, last = l - 1, d, string = "";
for (let i = 0; i < l; i++) {
const b = points[i];
const prev = closed2 ? mod(i - 1, l) : Math.max(i - 1, 0);
const a = points[prev];
let command, c;
let vx, vy, ux, uy, ar, bl, br, cl;
let rx, ry, xAxisRotation, largeArcFlag, sweepFlag;
let x = toFixed(b.x);
let y = toFixed(b.y);
switch (b.command) {
case Commands.close:
command = Commands.close;
break;
case Commands.arc:
rx = b.rx;
ry = b.ry;
xAxisRotation = b.xAxisRotation;
largeArcFlag = b.largeArcFlag;
sweepFlag = b.sweepFlag;
command = Commands.arc + " " + rx + " " + ry + " " + xAxisRotation + " " + largeArcFlag + " " + sweepFlag + " " + x + " " + y;
break;
case Commands.curve:
ar = a.controls && a.controls.right || Vector.zero;
bl = b.controls && b.controls.left || Vector.zero;
if (a.relative) {
vx = toFixed(ar.x + a.x);
vy = toFixed(ar.y + a.y);
} else {
vx = toFixed(ar.x);
vy = toFixed(ar.y);
}
if (b.relative) {
ux = toFixed(bl.x + b.x);
uy = toFixed(bl.y + b.y);
} else {
ux = toFixed(bl.x);
uy = toFixed(bl.y);
}
command = (i === 0 ? Commands.move : Commands.curve) + " " + vx + " " + vy + " " + ux + " " + uy + " " + x + " " + y;
break;
case Commands.move:
d = b;
command = Commands.move + " " + x + " " + y;
break;
default:
command = b.command + " " + x + " " + y;
}
if (i >= last && closed2) {
if (b.command === Commands.curve) {
c = d;
br = b.controls && b.controls.right || b;
cl = c.controls && c.controls.left || c;
if (b.relative) {
vx = toFixed(br.x + b.x);
vy = toFixed(br.y + b.y);
} else {
vx = toFixed(br.x);
vy = toFixed(br.y);
}
if (c.relative) {
ux = toFixed(cl.x + c.x);
uy = toFixed(cl.y + c.y);
} else {
ux = toFixed(cl.x);
uy = toFixed(cl.y);
}
x = toFixed(c.x);
y = toFixed(c.y);
command += " C " + vx + " " + vy + " " + ux + " " + uy + " " + x + " " + y;
}
if (b.command !== Commands.close) {
command += " Z";
}
}
string += command + " ";
}
return string;
},
pointsToString: function(points, size) {
let string = "";
const r = size * 0.5;
for (let i = 0; i < points.length; i++) {
const x = points[i].x;
const y = points[i].y - r;
string += Commands.move + " " + x + " " + y + " ";
string += "a " + r + " " + r + " 0 1 0 0.001 0 Z";
}
return string;
},
getClip: function(shape, domElement) {
let clip = shape._renderer.clip;
if (!clip) {
clip = shape._renderer.clip = svg.createElement("clipPath", {
"clip-rule": "nonzero"
});
}
if (clip.parentNode === null) {
domElement.defs.appendChild(clip);
}
return clip;
},
getRendererType: function(type) {
return type in svg ? type : "path";
},
defs: {
update: function(domElement) {
const { defs } = domElement;
if (defs._flagUpdate) {
const children = Array.prototype.slice.call(defs.children, 0);
for (let i = 0; i < children.length; i++) {
const child = children[i];
const id = child.id;
const selector = `[fill="url(#${id})"],[stroke="url(#${id})"],[clip-path="url(#${id})"]`;
const exists = domElement.querySelector(selector);
if (!exists) {
defs.removeChild(child);
}
}
defs._flagUpdate = false;
}
}
},
group: {
// TODO: Can speed up.
// TODO: How does this effect a f
appendChild: function(object) {
const elem = object._renderer.elem;
if (!elem) {
return;
}
const tag = elem.nodeName;
if (!tag || /(radial|linear)gradient/i.test(tag) || object._clip) {
return;
}
this.elem.appendChild(elem);
},
removeChild: function(object) {
const elem = object._renderer.elem;
if (!elem || elem.parentNode != this.elem) {
return;
}
const tag = elem.nodeName;
if (!tag) {
return;
}
if (object._clip) {
return;
}
this.elem.removeChild(elem);
},
orderChild: function(object) {
this.elem.appendChild(object._renderer.elem);
},
renderChild: function(child) {
const prop = svg.getRendererType(child._renderer.type);
svg[prop].render.call(child, this);
},
render: function(domElement) {
if (!this._visible && !this._flagVisible || this._opacity === 0 && !this._flagOpacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
if (!this._renderer.elem) {
this._renderer.elem = svg.createElement("g", {
id: this.id
});
domElement.appendChild(this._renderer.elem);
}
const flagMatrix = this._matrix.manual || this._flagMatrix;
const context = {
domElement,
elem: this._renderer.elem
};
if (flagMatrix) {
this._renderer.elem.setAttribute(
"transform",
"matrix(" + this._matrix.toString() + ")"
);
}
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
const prop = svg.getRendererType(child._renderer.type);
svg[prop].render.call(child, domElement);
}
if (this._flagId) {
this._renderer.elem.setAttribute("id", this._id);
}
if (this._flagOpacity) {
this._renderer.elem.setAttribute("opacity", this._opacity);
}
if (this._flagVisible) {
this._renderer.elem.setAttribute(
"display",
this._visible ? "inline" : "none"
);
}
if (this._flagClassName) {
this._renderer.elem.setAttribute("class", this.classList.join(" "));
}
if (this._flagAdditions) {
this.additions.forEach(svg.group.appendChild, context);
}
if (this._flagSubtractions) {
this.subtractions.forEach(svg.group.removeChild, context);
}
if (this._flagOrder) {
this.children.forEach(svg.group.orderChild, context);
}
if (this._flagMask) {
if (this._mask) {
const prop = svg.getRendererType(this._mask._renderer.type);
svg[prop].render.call(this._mask, domElement);
this._renderer.elem.setAttribute(
"clip-path",
"url(#" + this._mask.id + ")"
);
} else {
this._renderer.elem.removeAttribute("clip-path");
}
}
if (this.dataset) {
Object.assign(this._renderer.elem.dataset, this.dataset);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
path: {
render: function(domElement) {
if (this._opacity === 0 && !this._flagOpacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const changed = {};
const flagMatrix = this._matrix.manual || this._flagMatrix;
if (flagMatrix) {
changed.transform = "matrix(" + this._matrix.toString() + ")";
}
if (this._flagId) {
changed.id = this._id;
}
if (this._flagVertices) {
const vertices = svg.toString(this._renderer.vertices, this._closed);
changed.d = vertices;
}
if (this._fill && this._fill._renderer) {
this._renderer.hasFillEffect = true;
this._fill._update();
const prop = svg.getRendererType(this._fill._renderer.type);
svg[prop].render.call(this._fill, domElement, true);
}
if (this._flagFill) {
changed.fill = this._fill && this._fill.id ? "url(#" + this._fill.id + ")" : this._fill;
if (this._renderer.hasFillEffect && typeof this._fill.id === "undefined") {
domElement.defs._flagUpdate = true;
delete this._renderer.hasFillEffect;
}
}
if (this._stroke && this._stroke._renderer) {
this._renderer.hasStrokeEffect = true;
this._stroke._update();
const prop = svg.getRendererType(this._stroke._renderer.type);
svg[prop].render.call(this._stroke, domElement, true);
}
if (this._flagStroke) {
changed.stroke = this._stroke && this._stroke.id ? "url(#" + this._stroke.id + ")" : this._stroke;
if (this._renderer.hasStrokeEffect && typeof this._stroke.id === "undefined") {
domElement.defs._flagUpdate = true;
delete this._renderer.hasStrokeEffect;
}
}
if (this._flagLinewidth) {
changed["stroke-width"] = getEffectiveStrokeWidth(this);
}
if (this._flagOpacity) {
changed["stroke-opacity"] = this._opacity;
changed["fill-opacity"] = this._opacity;
}
if (this._flagClassName) {
changed["class"] = this.classList.join(" ");
}
if (this._flagVisible) {
changed.visibility = this._visible ? "visible" : "hidden";
}
if (this._flagCap) {
changed["stroke-linecap"] = this._cap;
}
if (this._flagJoin) {
changed["stroke-linejoin"] = this._join;
}
if (this._flagMiter) {
changed["stroke-miterlimit"] = this._miter;
}
if (this.dashes && this.dashes.length > 0) {
changed["stroke-dasharray"] = this.dashes.join(" ");
changed["stroke-dashoffset"] = this.dashes.offset || 0;
}
if (!this._renderer.elem) {
changed.id = this._id;
this._renderer.elem = svg.createElement("path", changed);
domElement.appendChild(this._renderer.elem);
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._flagClip) {
const clip = svg.getClip(this, domElement);
const elem = this._renderer.elem;
if (this._clip) {
elem.removeAttribute("id");
clip.setAttribute("id", this.id);
clip.appendChild(elem);
} else {
clip.removeAttribute("id");
elem.setAttribute("id", this.id);
this.parent._renderer.elem.appendChild(elem);
}
}
if (this._flagMask) {
if (this._mask) {
const prop = svg.getRendererType(this._mask._renderer.type);
svg[prop].render.call(this._mask, domElement);
this._renderer.elem.setAttribute(
"clip-path",
"url(#" + this._mask.id + ")"
);
} else {
this._renderer.elem.removeAttribute("clip-path");
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
points: {
render: function(domElement) {
if (this._opacity === 0 && !this._flagOpacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const changed = {};
const flagMatrix = this._matrix.manual || this._flagMatrix;
if (flagMatrix) {
changed.transform = "matrix(" + this._matrix.toString() + ")";
}
if (this._flagId) {
changed.id = this._id;
}
if (this._flagVertices || this._flagSize || this._flagSizeAttenuation) {
let size = this._size;
if (!this._sizeAttenuation) {
const me = this.worldMatrix.elements;
const m = decomposeMatrix(me[0], me[3], me[1], me[4], me[2], me[5]);
size /= Math.max(m.scaleX, m.scaleY);
}
const vertices = svg.pointsToString(this._renderer.collection, size);
changed.d = vertices;
}
if (this._fill && this._fill._renderer) {
this._renderer.hasFillEffect = true;
this._fill._update();
const prop = svg.getRendererType(this._fill._renderer.type);
svg[prop].render.call(this._fill, domElement, true);
}
if (this._flagFill) {
changed.fill = this._fill && this._fill.id ? "url(#" + this._fill.id + ")" : this._fill;
if (this._renderer.hasFillEffect && typeof this._fill.id === "undefined") {
domElement.defs._flagUpdate = true;
delete this._renderer.hasFillEffect;
}
}
if (this._stroke && this._stroke._renderer) {
this._renderer.hasStrokeEffect = true;
this._stroke._update();
const prop = svg.getRendererType(this._stroke._renderer.type);
svg[prop].render.call(this._stroke, domElement, true);
}
if (this._flagStroke) {
changed.stroke = this._stroke && this._stroke.id ? "url(#" + this._stroke.id + ")" : this._stroke;
if (this._renderer.hasStrokeEffect && typeof this._stroke.id === "undefined") {
domElement.defs._flagUpdate = true;
delete this._renderer.hasStrokeEffect;
}
}
if (this._flagLinewidth) {
changed["stroke-width"] = getEffectiveStrokeWidth(this);
}
if (this._flagOpacity) {
changed["stroke-opacity"] = this._opacity;
changed["fill-opacity"] = this._opacity;
}
if (this._flagClassName) {
changed["class"] = this.classList.join(" ");
}
if (this._flagVisible) {
changed.visibility = this._visible ? "visible" : "hidden";
}
if (this.dashes && this.dashes.length > 0) {
changed["stroke-dasharray"] = this.dashes.join(" ");
changed["stroke-dashoffset"] = this.dashes.offset || 0;
}
if (!this._renderer.elem) {
changed.id = this._id;
this._renderer.elem = svg.createElement("path", changed);
domElement.appendChild(this._renderer.elem);
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
text: {
render: function(domElement) {
this._update();
const changed = {};
const flagMatrix = this._matrix.manual || this._flagMatrix;
if (flagMatrix) {
changed.transform = "matrix(" + this._matrix.toString() + ")";
}
if (this._flagId) {
changed.id = this._id;
}
if (this._flagFamily) {
changed["font-family"] = this._family;
}
if (this._flagSize) {
changed["font-size"] = this._size;
}
if (this._flagLeading) {
changed["line-height"] = this._leading;
}
if (this._flagAlignment) {
changed["text-anchor"] = svg.alignments[this._alignment] || this._alignment;
}
if (this._flagBaseline) {
changed["dominant-baseline"] = svg.baselines[this._baseline] || this._baseline;
}
if (this._flagStyle) {
changed["font-style"] = this._style;
}
if (this._flagWeight) {
changed["font-weight"] = this._weight;
}
if (this._flagDecoration) {
changed["text-decoration"] = this._decoration;
}
if (this._flagDirection) {
changed["direction"] = this._direction;
}
if (this._fill && this._fill._renderer) {
this._renderer.hasFillEffect = true;
this._fill._update();
const prop = svg.getRendererType(this._fill._renderer.type);
svg[prop].render.call(this._fill, domElement, true);
}
if (this._flagFill) {
changed.fill = this._fill && this._fill.id ? "url(#" + this._fill.id + ")" : this._fill;
if (this._renderer.hasFillEffect && typeof this._fill.id === "undefined") {
domElement.defs._flagUpdate = true;
delete this._renderer.hasFillEffect;
}
}
if (this._stroke && this._stroke._renderer) {
this._renderer.hasStrokeEffect = true;
this._stroke._update();
const prop = svg.getRendererType(this._stroke._renderer.type);
svg[prop].render.call(this._stroke, domElement, true);
}
if (this._flagStroke) {
changed.stroke = this._stroke && this._stroke.id ? "url(#" + this._stroke.id + ")" : this._stroke;
if (this._renderer.hasStrokeEffect && typeof this._stroke.id === "undefined") {
domElement.defs._flagUpdate = true;
delete this._renderer.hasStrokeEffect;
}
}
if (this._flagLinewidth) {
changed["stroke-width"] = getEffectiveStrokeWidth(this);
}
if (this._flagOpacity) {
changed.opacity = this._opacity;
}
if (this._flagClassName) {
changed["class"] = this.classList.join(" ");
}
if (this._flagVisible) {
changed.visibility = this._visible ? "visible" : "hidden";
}
if (this.dashes && this.dashes.length > 0) {
changed["stroke-dasharray"] = this.dashes.join(" ");
changed["stroke-dashoffset"] = this.dashes.offset || 0;
}
if (!this._renderer.elem) {
changed.id = this._id;
this._renderer.elem = svg.createElement("text", changed);
domElement.appendChild(this._renderer.elem);
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._flagClip) {
const clip = svg.getClip(this, domElement);
const elem = this._renderer.elem;
if (this._clip) {
elem.removeAttribute("id");
clip.setAttribute("id", this.id);
clip.appendChild(elem);
} else {
clip.removeAttribute("id");
elem.setAttribute("id", this.id);
this.parent._renderer.elem.appendChild(elem);
}
}
if (this._flagMask) {
if (this._mask) {
const prop = svg.getRendererType(this._mask._renderer.type);
svg[prop].render.call(this._mask, domElement);
this._renderer.elem.setAttribute(
"clip-path",
"url(#" + this._mask.id + ")"
);
} else {
this._renderer.elem.removeAttribute("clip-path");
}
}
if (this._flagValue) {
this._renderer.elem.textContent = this._value;
}
return this.flagReset();
}
},
"linear-gradient": {
render: function(domElement, silent) {
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
if (!silent) {
this._update();
}
const changed = {};
if (this._flagId) {
changed.id = this._id;
}
if (this._flagEndPoints) {
changed.x1 = this.left._x;
changed.y1 = this.left._y;
changed.x2 = this.right._x;
changed.y2 = this.right._y;
}
if (this._flagSpread) {
changed.spreadMethod = this._spread;
}
if (this._flagUnits) {
changed.gradientUnits = this._units;
}
if (!this._renderer.elem) {
changed.id = this._id;
this._renderer.elem = svg.createElement("linearGradient", changed);
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._renderer.elem.parentNode === null) {
domElement.defs.appendChild(this._renderer.elem);
}
if (this._flagStops) {
const lengthChanged = this._renderer.elem.childNodes.length !== this.stops.length;
if (lengthChanged) {
while (this._renderer.elem.lastChild) {
this._renderer.elem.removeChild(this._renderer.elem.lastChild);
}
}
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
const attrs = {};
if (stop._flagOffset) {
attrs.offset = 100 * stop._offset + "%";
}
if (stop._flagColor) {
attrs["stop-color"] = stop._color;
}
if (stop._flagOpacity) {
attrs["stop-opacity"] = stop._opacity;
}
if (!stop._renderer.elem) {
stop._renderer.elem = svg.createElement("stop", attrs);
} else {
svg.setAttributes(stop._renderer.elem, attrs);
}
if (lengthChanged) {
this._renderer.elem.appendChild(stop._renderer.elem);
}
stop.flagReset();
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
"radial-gradient": {
render: function(domElement, silent) {
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
if (!silent) {
this._update();
}
const changed = {};
if (this._flagId) {
changed.id = this._id;
}
if (this._flagCenter) {
changed.cx = this.center._x;
changed.cy = this.center._y;
}
if (this._flagFocal) {
changed.fx = this.focal._x;
changed.fy = this.focal._y;
}
if (this._flagRadius) {
changed.r = this._radius;
}
if (this._flagSpread) {
changed.spreadMethod = this._spread;
}
if (this._flagUnits) {
changed.gradientUnits = this._units;
}
if (!this._renderer.elem) {
changed.id = this._id;
this._renderer.elem = svg.createElement("radialGradient", changed);
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._renderer.elem.parentNode === null) {
domElement.defs.appendChild(this._renderer.elem);
}
if (this._flagStops) {
const lengthChanged = this._renderer.elem.childNodes.length !== this.stops.length;
if (lengthChanged) {
while (this._renderer.elem.lastChild) {
this._renderer.elem.removeChild(this._renderer.elem.lastChild);
}
}
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
const attrs = {};
if (stop._flagOffset) {
attrs.offset = 100 * stop._offset + "%";
}
if (stop._flagColor) {
attrs["stop-color"] = stop._color;
}
if (stop._flagOpacity) {
attrs["stop-opacity"] = stop._opacity;
}
if (!stop._renderer.elem) {
stop._renderer.elem = svg.createElement("stop", attrs);
} else {
svg.setAttributes(stop._renderer.elem, attrs);
}
if (lengthChanged) {
this._renderer.elem.appendChild(stop._renderer.elem);
}
stop.flagReset();
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
texture: {
render: function(domElement, silent) {
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
if (!silent) {
this._update();
}
const changed = {};
const styles = { x: 0, y: 0 };
const image = this.image;
if (this._flagId) {
changed.id = this._id;
}
if (this._flagLoaded && this.loaded) {
switch (image.nodeName.toLowerCase()) {
case "canvas":
styles.href = styles["xlink:href"] = image.toDataURL("image/png");
break;
case "img":
case "image":
styles.href = styles["xlink:href"] = this.src;
break;
}
}
if (this._flagOffset || this._flagLoaded || this._flagScale) {
changed.x = this._offset.x;
changed.y = this._offset.y;
if (image) {
changed.x -= image.width / 2;
changed.y -= image.height / 2;
if (this._scale instanceof Vector) {
changed.x *= this._scale.x;
changed.y *= this._scale.y;
} else {
changed.x *= this._scale;
changed.y *= this._scale;
}
}
if (changed.x > 0) {
changed.x *= -1;
}
if (changed.y > 0) {
changed.y *= -1;
}
}
if (this._flagScale || this._flagLoaded || this._flagRepeat) {
changed.width = 0;
changed.height = 0;
if (image) {
changed.width = image.width;
changed.height = image.height;
switch (this._repeat) {
case "no-repeat":
changed.width += 1;
changed.height += 1;
break;
}
if (this._scale instanceof Vector) {
changed.width *= this._scale.x;
changed.height *= this._scale.y;
} else {
changed.width *= this._scale;
changed.height *= this._scale;
}
if (/no-repeat/i.test(this._repeat)) {
styles.preserveAspectRatio = "xMidYMid";
} else {
styles.preserveAspectRatio = "none";
}
styles.width = changed.width;
styles.height = changed.height;
}
}
if (this._flagScale || this._flagLoaded) {
if (!this._renderer.image) {
this._renderer.image = svg.createElement("image", styles);
} else {
svg.setAttributes(this._renderer.image, styles);
}
}
if (!this._renderer.elem) {
changed.id = this._id;
changed.patternUnits = "userSpaceOnUse";
this._renderer.elem = svg.createElement("pattern", changed);
} else if (Object.keys(changed).length !== 0) {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._renderer.elem.parentNode === null) {
domElement.defs.appendChild(this._renderer.elem);
}
if (this._renderer.elem && this._renderer.image && !this._renderer.appended) {
this._renderer.elem.appendChild(this._renderer.image);
this._renderer.appended = true;
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
}
};
var Renderer2 = class extends Events {
constructor(params) {
super();
this.domElement = params.domElement || svg.createElement("svg");
this.scene = new Group();
this.scene.parent = this;
this.defs = svg.createElement("defs");
this.defs._flagUpdate = false;
this.domElement.appendChild(this.defs);
this.domElement.defs = this.defs;
this.domElement.style.overflow = "hidden";
}
/**
* @name Two.SVGRenderer#setSize
* @function
* @param {Number} width - The new width of the renderer.
* @param {Number} height - The new height of the renderer.
* @description Change the size of the renderer.
* @nota-bene Triggers a `Two.Events.resize`.
*/
setSize(width, height) {
this.width = width;
this.height = height;
svg.setAttributes(this.domElement, {
width,
height
});
return this.trigger(Events.Types.resize, width, height);
}
/**
* @name Two.SVGRenderer#render
* @function
* @description Render the current scene to the ``.
*/
render() {
svg.group.render.call(this.scene, this.domElement);
svg.defs.update(this.domElement);
return this;
}
};
/**
* @name Two.SVGRenderer.Utils
* @property {Object} - A massive object filled with utility functions and properties to render Two.js objects to a ``.
*/
__publicField(Renderer2, "Utils", svg);
// src/utils/shaders.js
var shaders = {
create: function(gl, source, type) {
const shader = gl.createShader(gl[type]);
gl.shaderSource(shader, source);
gl.compileShader(shader);
const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
const error = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new TwoError("unable to compile shader " + shader + ": " + error);
}
return shader;
},
types: {
vertex: "VERTEX_SHADER",
fragment: "FRAGMENT_SHADER"
},
path: {
vertex: `
precision mediump float;
attribute vec2 a_position;
uniform mat3 u_matrix;
uniform vec2 u_resolution;
uniform vec4 u_rect;
varying vec2 v_textureCoords;
void main() {
vec2 rectCoords = (a_position * (u_rect.zw - u_rect.xy)) + u_rect.xy;
vec2 projected = (u_matrix * vec3(rectCoords, 1.0)).xy;
vec2 normal = projected / u_resolution;
vec2 clipspace = (normal * 2.0) - 1.0;
gl_Position = vec4(clipspace * vec2(1.0, -1.0), 0.0, 1.0);
v_textureCoords = a_position;
}
`,
fragment: `
precision mediump float;
uniform sampler2D u_image;
varying vec2 v_textureCoords;
void main() {
vec4 texel = texture2D(u_image, v_textureCoords);
if (texel.a == 0.0) {
discard;
}
gl_FragColor = texel;
}
`
},
points: {
vertex: `
precision mediump float;
attribute vec2 a_position;
uniform float u_size;
uniform mat3 u_matrix;
uniform vec2 u_resolution;
varying vec2 v_textureCoords;
void main() {
vec2 projected = (u_matrix * vec3(a_position, 1.0)).xy;
vec2 normal = projected / u_resolution;
vec2 clipspace = (normal * 2.0) - 1.0;
gl_PointSize = u_size;
gl_Position = vec4(clipspace * vec2(1.0, -1.0), 0.0, 1.0);
v_textureCoords = a_position;
}
`,
fragment: `
precision mediump float;
uniform sampler2D u_image;
void main() {
vec4 texel = texture2D(u_image, gl_PointCoord);
if (texel.a == 0.0) {
discard;
}
gl_FragColor = texel;
}
`
}
};
// src/renderers/webgl.js
var multiplyMatrix = Matrix2.Multiply;
var identity = [1, 0, 0, 0, 1, 0, 0, 0, 1];
var transformation = new NumArray(9);
var CanvasUtils = Renderer.Utils;
var vector2 = new Vector();
var quad = new NumArray([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]);
var webgl = {
precision: 0.9,
isHidden: /(undefined|none|transparent)/i,
canvas: root.document ? root.document.createElement("canvas") : { getContext: function() {
} },
alignments: {
left: "start",
middle: "center",
right: "end"
},
matrix: new Matrix2(),
group: {
removeChild: function(child, gl) {
if (child.children) {
for (let i = 0; i < child.children.length; i++) {
webgl.group.removeChild(child.children[i], gl);
}
}
if (child._renderer.texture) {
gl.deleteTexture(child._renderer.texture);
delete child._renderer.texture;
}
if (child._renderer.positionBuffer) {
gl.deleteBuffer(child._renderer.positionBuffer);
delete child._renderer.positionBuffer;
}
},
/**
* @function
// * @type {(gl: any, programs: any) => any}
* @param {WebGLContext} gl
* @param {Object} programs
*/
render: function(gl, programs) {
if (!this._visible) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const parent = this.parent;
const flagParentMatrix = parent._matrix && parent._matrix.manual || parent._flagMatrix;
const flagMatrix = this._matrix.manual || this._flagMatrix;
if (flagParentMatrix || flagMatrix) {
if (!this._renderer.matrix) {
this._renderer.matrix = new NumArray(9);
}
this._matrix.toTransformArray(true, transformation);
multiplyMatrix(
transformation,
parent._renderer.matrix,
this._renderer.matrix
);
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
if (this._scale instanceof Vector) {
this._renderer.scale.x = this._scale.x;
this._renderer.scale.y = this._scale.y;
} else {
this._renderer.scale.x = this._scale;
this._renderer.scale.y = this._scale;
}
if (!/renderer/i.test(parent._renderer.type)) {
this._renderer.scale.x *= parent._renderer.scale.x;
this._renderer.scale.y *= parent._renderer.scale.y;
}
if (flagParentMatrix) {
this._flagMatrix = true;
}
}
if (this._mask) {
gl.clear(gl.STENCIL_BUFFER_BIT);
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.ALWAYS, 1, 0);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
gl.colorMask(false, false, false, false);
const prop = Renderer.Utils.getRendererType(
this._mask._renderer.type
);
webgl[prop].render.call(this._mask, gl, programs, this);
gl.stencilFunc(gl.EQUAL, 1, 255);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
gl.colorMask(true, true, true, true);
}
this._flagOpacity = parent._flagOpacity || this._flagOpacity;
this._renderer.opacity = this._opacity * (parent && parent._renderer ? parent._renderer.opacity : 1);
let i;
if (this._flagSubtractions) {
for (i = 0; i < this.subtractions.length; i++) {
webgl.group.removeChild(this.subtractions[i], gl);
}
}
for (i = 0; i < this.children.length; i++) {
const child = this.children[i];
const prop = Renderer.Utils.getRendererType(child._renderer.type);
webgl[prop].render.call(child, gl, programs);
}
if (this._mask) {
gl.disable(gl.STENCIL_TEST);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
path: {
updateCanvas: function(gl, elem) {
let prev, a, c, ux, uy, vx, vy, ar, bl, br, cl, x, y;
let isOffset;
const commands = elem._renderer.vertices;
const canvas3 = this.canvas;
const ctx = this.ctx;
const ratio = gl.renderer.ratio;
const scale = vector2.copy(elem._renderer.scale).multiply(ratio);
const stroke = elem._stroke;
const linewidth = elem._linewidth;
const fill = elem._fill;
const opacity = elem._renderer.opacity || elem._opacity;
const cap = elem._cap;
const join = elem._join;
const miter = elem._miter;
const closed2 = elem._closed;
const dashes = elem.dashes;
const length = commands.length;
const last = length - 1;
canvas3.width = Math.max(
Math.ceil(elem._renderer.rect.width * scale.x),
1
);
canvas3.height = Math.max(
Math.ceil(elem._renderer.rect.height * scale.y),
1
);
const centroid = elem._renderer.rect.centroid;
const cx = centroid.x;
const cy = centroid.y;
ctx.clearRect(0, 0, canvas3.width, canvas3.height);
if (fill) {
if (typeof fill === "string") {
ctx.fillStyle = fill;
} else {
const prop = Renderer.Utils.getRendererType(
fill._renderer.type
);
webgl[prop].render.call(fill, ctx, elem);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === "string") {
ctx.strokeStyle = stroke;
} else {
const prop = Renderer.Utils.getRendererType(
stroke._renderer.type
);
webgl[prop].render.call(stroke, ctx, elem);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(elem);
}
if (miter) {
ctx.miterLimit = miter;
}
if (join) {
ctx.lineJoin = join;
}
if (!closed2 && cap) {
ctx.lineCap = cap;
}
}
if (typeof opacity === "number") {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
let d, rx, ry, xAxisRotation, largeArcFlag, sweepFlag, ax, ay;
ctx.save();
ctx.scale(scale.x, scale.y);
ctx.translate(cx, cy);
ctx.beginPath();
for (let i = 0; i < commands.length; i++) {
const b = commands[i];
x = b.x;
y = b.y;
switch (b.command) {
case Commands.close:
ctx.closePath();
break;
case Commands.arc:
rx = b.rx;
ry = b.ry;
xAxisRotation = b.xAxisRotation;
largeArcFlag = b.largeArcFlag;
sweepFlag = b.sweepFlag;
prev = closed2 ? mod(i - 1, length) : Math.max(i - 1, 0);
a = commands[prev];
ax = a.x;
ay = a.y;
CanvasUtils.renderSvgArcCommand(
ctx,
ax,
ay,
rx,
ry,
largeArcFlag,
sweepFlag,
xAxisRotation,
x,
y
);
break;
case Commands.curve:
prev = closed2 ? mod(i - 1, length) : Math.max(i - 1, 0);
a = commands[prev];
ar = a.controls && a.controls.right || Vector.zero;
bl = b.controls && b.controls.left || Vector.zero;
if (a._relative) {
vx = ar.x + a.x;
vy = ar.y + a.y;
} else {
vx = ar.x;
vy = ar.y;
}
if (b._relative) {
ux = bl.x + b.x;
uy = bl.y + b.y;
} else {
ux = bl.x;
uy = bl.y;
}
ctx.bezierCurveTo(vx, vy, ux, uy, x, y);
if (i >= last && closed2) {
c = d;
br = b.controls && b.controls.right || Vector.zero;
cl = c.controls && c.controls.left || Vector.zero;
if (b._relative) {
vx = br.x + b.x;
vy = br.y + b.y;
} else {
vx = br.x;
vy = br.y;
}
if (c._relative) {
ux = cl.x + c.x;
uy = cl.y + c.y;
} else {
ux = cl.x;
uy = cl.y;
}
x = c.x;
y = c.y;
ctx.bezierCurveTo(vx, vy, ux, uy, x, y);
}
break;
case Commands.line:
ctx.lineTo(x, y);
break;
case Commands.move:
d = b;
ctx.moveTo(x, y);
break;
}
}
if (closed2) {
ctx.closePath();
}
if (!webgl.isHidden.test(fill)) {
isOffset = fill._renderer && fill._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(fill._renderer.scale.x, fill._renderer.scale.y);
}
ctx.fill();
if (isOffset) {
ctx.restore();
}
}
if (!webgl.isHidden.test(stroke)) {
isOffset = stroke._renderer && stroke._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-stroke._renderer.offset.x, -stroke._renderer.offset.y);
ctx.scale(stroke._renderer.scale.x, stroke._renderer.scale.y);
ctx.lineWidth = linewidth / stroke._renderer.scale.x;
}
ctx.stroke();
if (isOffset) {
ctx.restore();
}
}
ctx.restore();
},
// Returns the rect of a set of verts. Typically takes vertices that are
// "centered" around 0 and returns them to be anchored upper-left.
getBoundingClientRect: function(vertices, border, rect) {
let left = Infinity, right = -Infinity, top = Infinity, bottom = -Infinity, width, height;
vertices.forEach(function(v) {
const x = v.x, y = v.y, controls = v.controls;
let a, b, c, d, cl, cr;
top = Math.min(y, top);
left = Math.min(x, left);
right = Math.max(x, right);
bottom = Math.max(y, bottom);
if (!v.controls) {
return;
}
cl = controls.left;
cr = controls.right;
if (!cl || !cr) {
return;
}
a = v._relative ? cl.x + x : cl.x;
b = v._relative ? cl.y + y : cl.y;
c = v._relative ? cr.x + x : cr.x;
d = v._relative ? cr.y + y : cr.y;
if (!a || !b || !c || !d) {
return;
}
top = Math.min(b, d, top);
left = Math.min(a, c, left);
right = Math.max(a, c, right);
bottom = Math.max(b, d, bottom);
});
if (typeof border === "number") {
top -= border;
left -= border;
right += border;
bottom += border;
}
width = right - left;
height = bottom - top;
rect.top = top;
rect.left = left;
rect.right = right;
rect.bottom = bottom;
rect.width = width;
rect.height = height;
if (!rect.centroid) {
rect.centroid = {};
}
rect.centroid.x = -left;
rect.centroid.y = -top;
},
render: function(gl, programs, forcedParent) {
if (!this._visible || !this._opacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const parent = forcedParent || this.parent;
const prop = Renderer.Utils.getRendererType(this._renderer.type);
const program = programs[prop];
const flagParentMatrix = parent._matrix.manual || parent._flagMatrix;
const flagMatrix = this._matrix.manual || this._flagMatrix;
const parentChanged = this._renderer.parent !== parent;
const flagTexture = this._flagVertices || this._flagFill || this._fill instanceof LinearGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagEndPoints) || this._fill instanceof RadialGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagRadius || this._fill._flagCenter || this._fill._flagFocal) || this._fill instanceof Texture && (this._fill._flagLoaded && this._fill.loaded || this._fill._flagImage || this._fill._flagVideo || this._fill._flagRepeat || this._fill._flagOffset || this._fill._flagScale) || this._stroke instanceof LinearGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagEndPoints) || this._stroke instanceof RadialGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagRadius || this._stroke._flagCenter || this._stroke._flagFocal) || this._stroke instanceof Texture && (this._stroke._flagLoaded && this._stroke.loaded || this._stroke._flagImage || this._stroke._flagVideo || this._stroke._flagRepeat || this._stroke._flagOffset || this._fill._flagScale) || this._flagStroke || this._flagLinewidth || this._flagOpacity || parent._flagOpacity || this._flagVisible || this._flagCap || this._flagJoin || this._flagMiter || this._flagScale || this.dashes && this.dashes.length > 0 || !this._renderer.texture;
if (flagParentMatrix || flagMatrix || parentChanged) {
if (!this._renderer.matrix) {
this._renderer.matrix = new NumArray(9);
}
this._matrix.toTransformArray(true, transformation);
multiplyMatrix(
transformation,
parent._renderer.matrix,
this._renderer.matrix
);
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
let sx, sy;
if (this._scale instanceof Vector) {
sx = this._scale.x * parent._renderer.scale.x;
sy = this._scale.y * parent._renderer.scale.y;
} else {
sx = this._scale * parent._renderer.scale.x;
sy = this._scale * parent._renderer.scale.y;
}
this._renderer.scale.x = sx < 0 ? -sx : sx;
this._renderer.scale.y = sy < 0 ? -sy : sy;
if (parentChanged) {
this._renderer.parent = parent;
}
}
if (this._mask) {
gl.clear(gl.STENCIL_BUFFER_BIT);
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.ALWAYS, 1, 0);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
gl.colorMask(false, false, false, false);
const prop2 = Renderer.Utils.getRendererType(
this._mask._renderer.type
);
webgl[prop2].render.call(this._mask, gl, programs, this);
gl.stencilFunc(gl.EQUAL, 1, 255);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
gl.colorMask(true, true, true, true);
}
if (flagTexture) {
if (!this._renderer.rect) {
this._renderer.rect = {};
}
this._renderer.opacity = this._opacity * parent._renderer.opacity;
webgl.path.getBoundingClientRect(
this._renderer.vertices,
this._linewidth,
this._renderer.rect
);
webgl.updateTexture.call(webgl, gl, this);
} else {
if (this._fill && this._fill._update) {
this._fill._update();
}
if (this._stroke && this._stroke._update) {
this._stroke._update();
}
}
if (this._clip && !forcedParent || !this._renderer.texture) {
return this;
}
if (programs.current !== program) {
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, programs.buffers.position);
gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.position);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
if (!programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, "u_resolution"),
programs.resolution.width,
programs.resolution.height
);
}
programs.current = program;
}
if (programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, "u_resolution"),
programs.resolution.width,
programs.resolution.height
);
}
gl.bindTexture(gl.TEXTURE_2D, this._renderer.texture);
const rect = this._renderer.rect;
gl.uniformMatrix3fv(program.matrix, false, this._renderer.matrix);
gl.uniform4f(program.rect, rect.left, rect.top, rect.right, rect.bottom);
gl.drawArrays(gl.TRIANGLES, 0, 6);
if (this._mask) {
gl.disable(gl.STENCIL_TEST);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
points: {
// The canvas is a texture that is a rendering of one vertex
updateCanvas: function(gl, elem) {
let isOffset;
const canvas3 = this.canvas;
const ctx = this.ctx;
const ratio = gl.renderer.ratio;
const stroke = elem._stroke;
const linewidth = elem._linewidth;
const fill = elem._fill;
const opacity = elem._renderer.opacity || elem._opacity;
const dashes = elem.dashes;
const size = elem._size * ratio;
let dimension = size;
if (!webgl.isHidden.test(stroke)) {
dimension += linewidth;
}
canvas3.width = getPoT(dimension);
canvas3.height = canvas3.width;
const aspect = dimension / canvas3.width;
const cx = canvas3.width / 2;
const cy = canvas3.height / 2;
ctx.clearRect(0, 0, canvas3.width, canvas3.height);
if (fill) {
if (typeof fill === "string") {
ctx.fillStyle = fill;
} else {
const prop = Renderer.Utils.getRendererType(
fill._renderer.type
);
webgl[prop].render.call(fill, ctx, elem);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === "string") {
ctx.strokeStyle = stroke;
} else {
const prop = Renderer.Utils.getRendererType(
stroke._renderer.type
);
webgl[prop].render.call(stroke, ctx, elem);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(elem) / aspect;
}
}
if (typeof opacity === "number") {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
ctx.save();
ctx.translate(cx, cy);
ctx.scale(webgl.precision, webgl.precision);
ctx.beginPath();
ctx.arc(0, 0, size / aspect * 0.5, 0, TWO_PI);
ctx.restore();
if (closed) {
ctx.closePath();
}
if (!webgl.isHidden.test(fill)) {
isOffset = fill._renderer && fill._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(fill._renderer.scale.x, fill._renderer.scale.y);
}
ctx.fill();
if (isOffset) {
ctx.restore();
}
}
if (!webgl.isHidden.test(stroke)) {
isOffset = stroke._renderer && stroke._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-stroke._renderer.offset.x, -stroke._renderer.offset.y);
ctx.scale(stroke._renderer.scale.x, stroke._renderer.scale.y);
ctx.lineWidth = linewidth / stroke._renderer.scale.x;
}
ctx.stroke();
if (isOffset) {
ctx.restore();
}
}
},
render: function(gl, programs, forcedParent) {
if (!this._visible || !this._opacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
let size = this._size;
const parent = forcedParent || this.parent;
const program = programs[this._renderer.type];
const sizeAttenuation = this._sizeAttenuation;
const stroke = this._stroke;
const linewidth = this._linewidth;
const flagParentMatrix = parent._matrix.manual || parent._flagMatrix;
const flagMatrix = this._matrix.manual || this._flagMatrix;
const parentChanged = this._renderer.parent !== parent;
const commands = this._renderer.vertices;
const length = this._renderer.collection.length;
const flagVertices = this._flagVertices;
const flagTexture = this._flagFill || this._fill instanceof LinearGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagEndPoints) || this._fill instanceof RadialGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagRadius || this._fill._flagCenter || this._fill._flagFocal) || this._fill instanceof Texture && (this._fill._flagLoaded && this._fill.loaded || this._fill._flagImage || this._fill._flagVideo || this._fill._flagRepeat || this._fill._flagOffset || this._fill._flagScale) || this._stroke instanceof LinearGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagEndPoints) || this._stroke instanceof RadialGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagRadius || this._stroke._flagCenter || this._stroke._flagFocal) || this._stroke instanceof Texture && (this._stroke._flagLoaded && this._stroke.loaded || this._stroke._flagImage || this._stroke._flagVideo || this._stroke._flagRepeat || this._stroke._flagOffset || this._fill._flagScale) || this._flagStroke || this._flagLinewidth || this._flagOpacity || parent._flagOpacity || this._flagVisible || this._flagScale || this.dashes && this.dashes.length > 0 || !this._renderer.texture;
if (flagParentMatrix || flagMatrix || parentChanged) {
if (!this._renderer.matrix) {
this._renderer.matrix = new NumArray(9);
}
this._matrix.toTransformArray(true, transformation);
multiplyMatrix(
transformation,
parent._renderer.matrix,
this._renderer.matrix
);
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
let sx, sy;
if (this._scale instanceof Vector) {
sx = this._scale.x * parent._renderer.scale.x;
sy = this._scale.y * parent._renderer.scale.y;
} else {
sx = this._scale * parent._renderer.scale.x;
sy = this._scale * parent._renderer.scale.y;
}
this._renderer.scale.x = sx < 0 ? -sx : sx;
this._renderer.scale.y = sy < 0 ? -sy : sy;
if (parentChanged) {
this._renderer.parent = parent;
}
}
if (flagVertices) {
const positionBuffer = this._renderer.positionBuffer;
if (positionBuffer) {
gl.deleteBuffer(positionBuffer);
}
this._renderer.positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this._renderer.positionBuffer);
gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.position);
gl.bufferData(gl.ARRAY_BUFFER, commands, gl.STATIC_DRAW);
}
if (flagTexture) {
this._renderer.opacity = this._opacity * parent._renderer.opacity;
webgl.updateTexture.call(webgl, gl, this);
} else {
if (this._fill && this._fill._update) {
this._fill._update();
}
if (this._stroke && this._stroke._update) {
this._stroke._update();
}
}
if (this._clip && !forcedParent || !this._renderer.texture) {
return this;
}
if (!webgl.isHidden.test(stroke)) {
size += linewidth;
}
size /= webgl.precision;
if (sizeAttenuation) {
size *= Math.max(this._renderer.scale.x, this._renderer.scale.y);
}
if (programs.current !== program) {
gl.useProgram(program);
if (!programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, "u_resolution"),
programs.resolution.width,
programs.resolution.height
);
}
programs.current = program;
}
if (programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, "u_resolution"),
programs.resolution.width,
programs.resolution.height
);
}
gl.bindTexture(gl.TEXTURE_2D, this._renderer.texture);
gl.uniformMatrix3fv(program.matrix, false, this._renderer.matrix);
gl.uniform1f(program.size, size * programs.resolution.ratio);
gl.drawArrays(gl.POINTS, 0, length);
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
text: {
updateCanvas: function(gl, elem) {
const canvas3 = this.canvas;
const ctx = this.ctx;
const ratio = gl.renderer.ratio;
const scale = vector2.copy(elem._renderer.scale).multiply(ratio);
const stroke = elem._stroke;
const linewidth = elem._linewidth;
const fill = elem._fill;
const opacity = elem._renderer.opacity || elem._opacity;
const dashes = elem.dashes;
const decoration = elem._decoration;
const direction = elem._direction;
canvas3.width = Math.max(
Math.ceil(elem._renderer.rect.width * scale.x),
1
);
canvas3.height = Math.max(
Math.ceil(elem._renderer.rect.height * scale.y),
1
);
const centroid = elem._renderer.rect.centroid;
const cx = centroid.x;
const cy = centroid.y;
let a, b, c, d, e, sx, sy, x1, y1, x2, y2;
const isOffset = fill._renderer && fill._renderer.offset && stroke._renderer && stroke._renderer.offset;
ctx.clearRect(0, 0, canvas3.width, canvas3.height);
if (!isOffset) {
ctx.font = [
elem._style,
elem._weight,
elem._size + "px/" + elem._leading + "px",
elem._family
].join(" ");
}
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.textDirection = direction;
if (fill) {
if (typeof fill === "string") {
ctx.fillStyle = fill;
} else {
const prop = Renderer.Utils.getRendererType(
fill._renderer.type
);
webgl[prop].render.call(fill, ctx, elem);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === "string") {
ctx.strokeStyle = stroke;
} else {
const prop = Renderer.Utils.getRendererType(
stroke._renderer.type
);
webgl[prop].render.call(stroke, ctx, elem);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(elem);
}
}
if (typeof opacity === "number") {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
ctx.save();
ctx.scale(scale.x, scale.y);
ctx.translate(cx, cy);
if (!webgl.isHidden.test(fill)) {
if (fill._renderer && fill._renderer.offset) {
sx = fill._renderer.scale.x;
sy = fill._renderer.scale.y;
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(sx, sy);
a = elem._size / fill._renderer.scale.y;
b = elem._leading / fill._renderer.scale.y;
ctx.font = [
elem._style,
elem._weight,
a + "px/",
b + "px",
elem._family
].join(" ");
c = fill._renderer.offset.x / fill._renderer.scale.x;
d = fill._renderer.offset.y / fill._renderer.scale.y;
ctx.fillText(elem.value, c, d);
ctx.restore();
} else {
ctx.fillText(elem.value, 0, 0);
}
}
if (!webgl.isHidden.test(stroke)) {
if (stroke._renderer && stroke._renderer.offset) {
sx = stroke._renderer.scale.x;
sy = stroke._renderer.scale.y;
ctx.save();
ctx.translate(-stroke._renderer.offset.x, -stroke._renderer.offset.y);
ctx.scale(sx, sy);
a = elem._size / stroke._renderer.scale.y;
b = elem._leading / stroke._renderer.scale.y;
ctx.font = [
elem._style,
elem._weight,
a + "px/",
b + "px",
elem._family
].join(" ");
c = stroke._renderer.offset.x / stroke._renderer.scale.x;
d = stroke._renderer.offset.y / stroke._renderer.scale.y;
e = linewidth / stroke._renderer.scale.x;
ctx.lineWidth = e;
ctx.strokeText(elem.value, c, d);
ctx.restore();
} else {
ctx.strokeText(elem.value, 0, 0);
}
}
if (/(underline|strikethrough)/i.test(decoration)) {
const metrics = ctx.measureText(elem.value);
switch (decoration) {
case "underline":
y1 = metrics.actualBoundingBoxDescent;
y2 = metrics.actualBoundingBoxDescent;
break;
case "strikethrough":
y1 = 0;
y2 = 0;
break;
}
x1 = -metrics.width / 2;
x2 = metrics.width / 2;
ctx.lineWidth = Math.max(Math.floor(elem._size / 15), 1);
ctx.strokeStyle = ctx.fillStyle;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
ctx.restore();
},
getBoundingClientRect: function(elem, rect) {
const ctx = webgl.ctx;
ctx.font = [
elem._style,
elem._weight,
elem._size + "px/" + elem._leading + "px",
elem._family
].join(" ");
ctx.textAlign = "center";
ctx.textBaseline = Renderer.Utils.baselines[elem._baseline] || elem._baseline;
const metrics = ctx.measureText(elem._value);
let width = metrics.width;
let height = 1.15 * (metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent);
if (this._linewidth && !webgl.isHidden.test(this._stroke)) {
width += this._linewidth * 2;
height += this._linewidth * 2;
}
const w = width / 2;
const h = height / 2;
switch (webgl.alignments[elem._alignment] || elem._alignment) {
case webgl.alignments.left:
if (elem.direction === "ltr") {
rect.left = 0;
rect.right = width;
} else {
rect.left = -width;
rect.right = 0;
}
break;
case webgl.alignments.right:
if (elem.direction === "ltr") {
rect.left = -width;
rect.right = 0;
} else {
rect.left = 0;
rect.right = width;
}
break;
default:
rect.left = -w;
rect.right = w;
}
switch (elem._baseline) {
case "bottom":
rect.top = -height;
rect.bottom = 0;
break;
case "top":
rect.top = 0;
rect.bottom = height;
break;
case "baseline":
rect.top = -h * 1.5;
rect.bottom = h * 0.5;
break;
default:
rect.top = -h;
rect.bottom = h;
}
rect.width = width;
rect.height = height;
if (!rect.centroid) {
rect.centroid = {};
}
rect.centroid.x = w;
rect.centroid.y = h;
},
render: function(gl, programs, forcedParent) {
if (!this._visible || !this._opacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const parent = forcedParent || this.parent;
const program = programs[this._renderer.type];
const flagParentMatrix = parent._matrix.manual || parent._flagMatrix;
const flagMatrix = this._matrix.manual || this._flagMatrix;
const parentChanged = this._renderer.parent !== parent;
const flagTexture = this._flagVertices || this._flagFill || this._fill instanceof LinearGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagEndPoints) || this._fill instanceof RadialGradient && (this._fill._flagSpread || this._fill._flagStops || this._fill._flagRadius || this._fill._flagCenter || this._fill._flagFocal) || this._fill instanceof Texture && (this._fill._flagLoaded && this._fill.loaded || this._fill._flagImage || this._fill._flagVideo || this._fill._flagRepeat || this._fill._flagOffset || this._fill._flagScale) || this._stroke instanceof LinearGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagEndPoints) || this._stroke instanceof RadialGradient && (this._stroke._flagSpread || this._stroke._flagStops || this._stroke._flagRadius || this._stroke._flagCenter || this._stroke._flagFocal) || this._stroke instanceof Texture && (this._stroke._flagLoaded && this._stroke.loaded || this._stroke._flagImage || this._stroke._flagVideo || this._stroke._flagRepeat || this._stroke._flagOffset || this._fill._flagScale) || this._flagStroke || this._flagLinewidth || this._flagOpacity || parent._flagOpacity || this._flagVisible || this._flagScale || this._flagValue || this._flagFamily || this._flagSize || this._flagLeading || this._flagAlignment || this._flagBaseline || this._flagStyle || this._flagWeight || this._flagDecoration || this.dashes && this.dashes.length > 0 || !this._renderer.texture;
if (flagParentMatrix || flagMatrix || parentChanged) {
if (!this._renderer.matrix) {
this._renderer.matrix = new NumArray(9);
}
this._matrix.toTransformArray(true, transformation);
multiplyMatrix(
transformation,
parent._renderer.matrix,
this._renderer.matrix
);
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
let sx, sy;
if (this._scale instanceof Vector) {
sx = this._scale.x * parent._renderer.scale.x;
sy = this._scale.y * parent._renderer.scale.y;
} else {
sx = this._scale * parent._renderer.scale.x;
sy = this._scale * parent._renderer.scale.y;
}
this._renderer.scale.x = sx < 0 ? -sx : sx;
this._renderer.scale.y = sy < 0 ? -sy : sy;
if (parentChanged) {
this._renderer.parent = parent;
}
}
if (this._mask) {
gl.clear(gl.STENCIL_BUFFER_BIT);
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.ALWAYS, 1, 0);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
gl.colorMask(false, false, false, false);
const prop = Renderer.Utils.getRendererType(
this._mask._renderer.type
);
webgl[prop].render.call(this._mask, gl, programs, this);
gl.stencilFunc(gl.EQUAL, 1, 255);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
gl.colorMask(true, true, true, true);
}
if (flagTexture) {
if (!this._renderer.rect) {
this._renderer.rect = {};
}
this._renderer.opacity = this._opacity * parent._renderer.opacity;
webgl.text.getBoundingClientRect(this, this._renderer.rect);
webgl.updateTexture.call(webgl, gl, this);
} else {
if (this._fill && this._fill._update) {
this._fill._update();
}
if (this._stroke && this._stroke._update) {
this._stroke._update();
}
}
if (this._clip && !forcedParent || !this._renderer.texture) {
return this;
}
if (programs.current !== program) {
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, programs.buffers.position);
gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.position);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
if (!programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, "u_resolution"),
programs.resolution.width,
programs.resolution.height
);
}
programs.current = program;
}
if (programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, "u_resolution"),
programs.resolution.width,
programs.resolution.height
);
}
gl.bindTexture(gl.TEXTURE_2D, this._renderer.texture);
const rect = this._renderer.rect;
gl.uniformMatrix3fv(program.matrix, false, this._renderer.matrix);
gl.uniform4f(program.rect, rect.left, rect.top, rect.right, rect.bottom);
gl.drawArrays(gl.TRIANGLES, 0, 6);
if (this._mask) {
gl.disable(gl.STENCIL_TEST);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
"linear-gradient": {
render: function(ctx, parent) {
if (!ctx.canvas.getContext("2d") || !parent) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
if (!this._renderer.effect || this._flagEndPoints || this._flagStops || this._flagUnits) {
let rect;
let lx = this.left._x;
let ly = this.left._y;
let rx = this.right._x;
let ry = this.right._y;
if (/objectBoundingBox/i.test(this._units)) {
rect = parent.getBoundingClientRect(true);
lx = (lx - 0.5) * rect.width;
ly = (ly - 0.5) * rect.height;
rx = (rx - 0.5) * rect.width;
ry = (ry - 0.5) * rect.height;
}
this._renderer.effect = ctx.createLinearGradient(lx, ly, rx, ry);
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
this._renderer.effect.addColorStop(stop._offset, stop._color);
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
"radial-gradient": {
render: function(ctx, parent) {
if (!ctx.canvas.getContext("2d") || !parent) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
if (!this._renderer.effect || this._flagCenter || this._flagFocal || this._flagRadius || this._flagStops || this._flagUnits) {
let rect;
let cx = this.center._x;
let cy = this.center._y;
let fx = this.focal._x;
let fy = this.focal._y;
let radius = this._radius;
if (/objectBoundingBox/i.test(this._units)) {
rect = parent.getBoundingClientRect(true);
cx = (cx - 0.5) * rect.width * 0.5;
cy = (cy - 0.5) * rect.height * 0.5;
fx = (fx - 0.5) * rect.width * 0.5;
fy = (fy - 0.5) * rect.height * 0.5;
radius *= Math.min(rect.width, rect.height);
}
this._renderer.effect = ctx.createRadialGradient(
cx,
cy,
0,
fx,
fy,
radius
);
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
this._renderer.effect.addColorStop(stop._offset, stop._color);
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
texture: {
render: function(ctx, elem) {
if (!ctx.canvas.getContext("2d")) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const image = this.image;
if ((this._flagLoaded || this._flagImage || this._flagVideo || this._flagRepeat) && this.loaded) {
this._renderer.effect = ctx.createPattern(image, this._repeat);
} else if (!this._renderer.effect) {
return this.flagReset();
}
if (this._flagOffset || this._flagLoaded || this._flagScale) {
if (!(this._renderer.offset instanceof Vector)) {
this._renderer.offset = new Vector();
}
this._renderer.offset.x = -this._offset.x;
this._renderer.offset.y = -this._offset.y;
if (image) {
this._renderer.offset.x += image.width / 2;
this._renderer.offset.y += image.height / 2;
if (this._scale instanceof Vector) {
this._renderer.offset.x *= this._scale.x;
this._renderer.offset.y *= this._scale.y;
} else {
this._renderer.offset.x *= this._scale;
this._renderer.offset.y *= this._scale;
}
}
}
if (this._flagScale || this._flagLoaded) {
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
let sx, sy;
if (this._scale instanceof Vector) {
sx = this._scale.x;
sy = this._scale.y;
} else {
sx = this._scale;
sy = this._scale;
}
this._renderer.scale.x = sx < 0 ? -sx : sx;
this._renderer.scale.y = sy < 0 ? -sy : sy;
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
}
},
updateTexture: function(gl, elem) {
const prop = Renderer.Utils.getRendererType(elem._renderer.type);
this[prop].updateCanvas.call(webgl, gl, elem);
if (this.canvas.width <= 0 || this.canvas.height <= 0) {
if (elem._renderer.texture) {
gl.deleteTexture(elem._renderer.texture);
}
delete elem._renderer.texture;
return;
}
if (!elem._renderer.texture) {
elem._renderer.texture = gl.createTexture();
}
gl.bindTexture(gl.TEXTURE_2D, elem._renderer.texture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
this.canvas
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
},
program: {
create: function(gl, shaders2) {
let program, linked, error;
program = gl.createProgram();
_.each(shaders2, function(s) {
gl.attachShader(program, s);
});
gl.linkProgram(program);
linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
error = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw new TwoError("unable to link program: " + error);
}
return program;
}
},
extensions: {
init: function(gl) {
const extensions = {};
const names = [
"EXT_texture_filter_anisotropic",
"WEBGL_compressed_texture_s3tc",
"OES_texture_float_linear",
"WEBGL_multisampled_render_to_texture"
];
for (let i = 0; i < names.length; i++) {
const name = names[i];
extensions[name] = webgl.extensions.get(gl, name);
}
return extensions;
},
get: function(gl, name) {
return gl.getExtension(name) || gl.getExtension(`MOZ_${name}`) || gl.getExtension(`WEBKIT_${name}`);
}
},
TextureRegistry: new Registry()
};
webgl.ctx = webgl.canvas.getContext("2d");
var Renderer3 = class extends Events {
constructor(params) {
super();
let gl, program, vs, fs;
this.domElement = params.domElement || document.createElement("canvas");
if (typeof params.offscreenElement !== "undefined") {
webgl.canvas = params.offscreenElement;
webgl.ctx = webgl.canvas.getContext("2d");
}
this.scene = new Group();
this.scene.parent = this;
this._renderer = {
type: "renderer",
matrix: new NumArray(identity),
scale: 1,
opacity: 1
};
this._flagMatrix = true;
params = _.defaults(params || {}, {
antialias: false,
alpha: true,
premultipliedAlpha: true,
stencil: true,
preserveDrawingBuffer: true,
overdraw: false
});
this.overdraw = params.overdraw;
gl = this.ctx = this.domElement.getContext("webgl", params) || this.domElement.getContext("experimental-webgl", params);
if (!this.ctx) {
throw new TwoError(
"unable to create a webgl context. Try using another renderer."
);
}
vs = shaders.create(gl, shaders.path.vertex, shaders.types.vertex);
fs = shaders.create(gl, shaders.path.fragment, shaders.types.fragment);
this.programs = {
current: null,
buffers: {
position: gl.createBuffer()
},
resolution: {
width: 0,
height: 0,
ratio: 1,
flagged: false
}
};
program = this.programs.path = webgl.program.create(gl, [vs, fs]);
this.programs.text = this.programs.path;
gl.extensions = webgl.extensions.init(gl);
gl.renderer = this;
program.position = gl.getAttribLocation(program, "a_position");
program.matrix = gl.getUniformLocation(program, "u_matrix");
program.rect = gl.getUniformLocation(program, "u_rect");
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.position);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
vs = shaders.create(gl, shaders.points.vertex, shaders.types.vertex);
fs = shaders.create(gl, shaders.points.fragment, shaders.types.fragment);
program = this.programs.points = webgl.program.create(gl, [vs, fs]);
program.position = gl.getAttribLocation(program, "a_position");
program.matrix = gl.getUniformLocation(program, "u_matrix");
program.size = gl.getUniformLocation(program, "u_size");
gl.enable(gl.BLEND);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
}
/**
* @name Two.WebGLRenderer#setSize
* @function
* @fires resize
* @param {Number} width - The new width of the renderer.
* @param {Number} height - The new height of the renderer.
* @param {Number} [ratio] - The new pixel ratio (pixel density) of the renderer. Defaults to calculate the pixel density of the user's screen.
* @description Change the size of the renderer.
*/
setSize(width, height, ratio) {
let w, h;
const ctx = this.ctx;
this.width = width;
this.height = height;
this.ratio = typeof ratio === "undefined" ? getRatio(ctx) : ratio;
this.domElement.width = width * this.ratio;
this.domElement.height = height * this.ratio;
if (_.isObject(this.domElement.style)) {
_.extend(this.domElement.style, {
width: width + "px",
height: height + "px"
});
}
this._renderer.matrix[0] = this._renderer.matrix[4] = this._renderer.scale = this.ratio;
this._flagMatrix = true;
w = width * this.ratio;
h = height * this.ratio;
ctx.viewport(0, 0, w, h);
this.programs.resolution.width = w;
this.programs.resolution.height = h;
this.programs.resolution.ratio = this.ratio;
this.programs.resolution.flagged = true;
return this.trigger(Events.Types.resize, width, height, ratio);
}
/**
* @name Two.WebGLRenderer#render
* @function
* @description Render the current scene to the ``.
*/
render() {
const gl = this.ctx;
if (!this.overdraw) {
gl.clear(gl.COLOR_BUFFER_BIT);
}
webgl.group.render.call(this.scene, gl, this.programs);
this._flagMatrix = false;
this.programs.resolution.flagged = true;
return this;
}
};
/**
* @name Two.WebGLRenderer.Utils
* @property {Object} - A massive object filled with utility functions and properties to render Two.js objects to a `` through the WebGL API.
*/
__publicField(Renderer3, "Utils", webgl);
// src/two.js
var Utils = _.extend(
{
Error: TwoError,
getRatio,
read,
xhr
},
_,
CanvasPolyfill,
curves_exports,
math_exports
);
var _Two = class _Two {
constructor(options) {
// Warning: inherit events while overriding static properties
/**
* @private
*/
__publicField(this, "_events", new Events());
/**
* @name Two#type
* @property {String} - A string representing which type of renderer the instance has instantiated.
*/
__publicField(this, "type", "");
/**
* @name Two#renderer
* @property {(Two.SVGRenderer|Two.CanvasRenderer|Two.WebGLRenderer)} - The instantiated rendering class for the instance. For a list of possible rendering types check out Two.Types.
*/
__publicField(this, "renderer", null);
/**
* @name Two#scene
* @property {Two.Group} - The base level {@link Two.Group} which houses all objects for the instance. Because it is a {@link Two.Group} transformations can be applied to it that will affect all objects in the instance. This is handy as a makeshift inverted camera.
*/
__publicField(this, "scene", null);
/**
* @name Two#width
* @property {Number} - The width of the instance's dom element.
*/
__publicField(this, "width", 0);
/**
* @name Two#height
* @property {Number} - The height of the instance's dom element.
*/
__publicField(this, "height", 0);
/**
* @name Two#frameCount
* @property {Number} - An integer representing how many frames have elapsed.
*/
__publicField(this, "frameCount", 0);
/**
* @name Two#timeDelta
* @property {Number} - A number representing how much time has elapsed since the last frame in milliseconds.
*/
__publicField(this, "timeDelta", 0);
/**
* @name Two#playing
* @property {Boolean} - A boolean representing whether or not the instance is being updated through the automatic `requestAnimationFrame`.
*/
__publicField(this, "playing", false);
const params = _.defaults(options || {}, {
fullscreen: false,
fitted: false,
width: 640,
height: 480,
type: _Two.Types.svg,
autostart: false
});
_.each(
params,
function(v, k) {
if (/fullscreen/i.test(k) || /autostart/i.test(k)) {
return;
}
this[k] = v;
},
this
);
if (_.isElement(params.domElement)) {
const tagName = params.domElement.tagName.toLowerCase();
if (!/^(CanvasRenderer-canvas|WebGLRenderer-canvas|SVGRenderer-svg)$/.test(
this.type + "-" + tagName
)) {
this.type = _Two.Types[tagName];
}
}
this.renderer = new _Two[this.type](this);
this.setPlaying(params.autostart);
this.frameCount = 0;
if (params.fullscreen) {
this.fit = fitToWindow.bind(this);
this.fit.domElement = window;
this.fit.attached = true;
_.extend(document.body.style, {
overflow: "hidden",
margin: 0,
padding: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
position: "fixed"
});
_.extend(this.renderer.domElement.style, {
display: "block",
top: 0,
left: 0,
right: 0,
bottom: 0,
position: "fixed"
});
dom.bind(this.fit.domElement, "resize", this.fit);
this.fit();
} else if (params.fitted) {
this.fit = fitToParent.bind(this);
_.extend(this.renderer.domElement.style, {
display: "block"
});
} else if (typeof params.width === "number" && typeof params.height === "number") {
this.renderer.setSize(params.width, params.height, this.ratio);
this.width = params.width;
this.height = params.height;
}
this.renderer.bind(Events.Types.resize, updateDimensions.bind(this));
this.scene = this.renderer.scene;
_Two.Instances.push(this);
if (params.autostart) {
raf.init();
}
}
// Getters and setters aren't enumerable
get _bound() {
return this._events._bound;
}
set _bound(v) {
this._events._bound = v;
}
addEventListener() {
var _a;
return (_a = this._events.addEventListener) == null ? void 0 : _a.apply(this, arguments);
}
on() {
var _a;
return (_a = this._events.addEventListener) == null ? void 0 : _a.apply(this, arguments);
}
bind() {
var _a;
return (_a = this._events.addEventListener) == null ? void 0 : _a.apply(this, arguments);
}
removeEventListener() {
var _a;
return (_a = this._events.removeEventListener) == null ? void 0 : _a.apply(this, arguments);
}
off() {
var _a;
return (_a = this._events.removeEventListener) == null ? void 0 : _a.apply(this, arguments);
}
unbind() {
var _a;
return (_a = this._events.removeEventListener) == null ? void 0 : _a.apply(this, arguments);
}
dispatchEvent() {
var _a;
return (_a = this._events.dispatchEvent) == null ? void 0 : _a.apply(this, arguments);
}
trigger() {
var _a;
return (_a = this._events.dispatchEvent) == null ? void 0 : _a.apply(this, arguments);
}
listen() {
var _a;
return (_a = this._events.listen) == null ? void 0 : _a.apply(this, arguments);
}
ignore() {
var _a;
return (_a = this._events.ignore) == null ? void 0 : _a.apply(this, arguments);
}
/**
* @name Two#appendTo
* @function
* @param {Element} elem - The DOM element to append the Two.js stage to.
* @description Shorthand method to append your instance of Two.js to the `document`.
*/
appendTo(elem) {
elem.appendChild(this.renderer.domElement);
if (this.fit) {
if (this.fit.domElement !== window) {
this.fit.domElement = elem;
this.fit.attached = false;
}
this.update();
}
return this;
}
/**
* @name Two#play
* @function
* @fires play
* @description Call to start an internal animation loop.
* @nota-bene This function initiates a `requestAnimationFrame` loop.
*/
play() {
this.playing = true;
raf.init();
return this.trigger(Events.Types.play);
}
/**
* @name Two#pause
* @function
* @fires pause
* @description Call to stop the internal animation loop for a specific instance of Two.js.
*/
pause() {
this.playing = false;
return this.trigger(Events.Types.pause);
}
setPlaying(p) {
this.playing = p;
}
/**
* @name Two#release
* @function
* @param {Two.Element} [obj] - Object to release from event listening. If none provided then the root {@link Two.Group} will be used.
* @returns {Two.Element} The object passed for event deallocation.
* @description Release a {@link Two.Element}’s events from memory and recurse through its children, effects, and/or vertices.
*/
release(obj) {
let i, v, child;
if (typeof obj === "undefined") {
return this.release(this.scene);
}
if (typeof obj.unbind === "function") {
obj.unbind();
}
if (typeof obj.fill === "object" && typeof obj.fill.unbind === "function") {
obj.fill.unbind();
}
if (typeof obj.stroke === "object" && typeof obj.stroke.unbind === "function") {
obj.stroke.unbind();
}
if (obj.vertices) {
if (typeof obj.vertices.unbind === "function") {
try {
obj.vertices.unbind();
} catch (e) {
}
}
for (i = 0; i < obj.vertices.length; i++) {
v = obj.vertices[i];
if (typeof v.unbind === "function") {
v.unbind();
}
if (v.controls) {
if (v.controls.left && typeof v.controls.left.unbind === "function") {
v.controls.left.unbind();
}
if (v.controls.right && typeof v.controls.right.unbind === "function") {
v.controls.right.unbind();
}
}
}
}
if (obj.children) {
for (i = 0; i < obj.children.length; i++) {
child = obj.children[i];
this.release(child);
}
if (typeof obj.children.unbind === "function") {
try {
obj.children.unbind();
} catch (e) {
}
}
}
if (obj._renderer) {
if (obj._renderer.elem && obj._renderer.elem.parentNode) {
obj._renderer.elem.parentNode.removeChild(obj._renderer.elem);
delete obj._renderer.elem;
}
if (this.type === "WebGLRenderer" && this.renderer.ctx) {
const gl = this.renderer.ctx;
if (obj._renderer.texture) {
gl.deleteTexture(obj._renderer.texture);
delete obj._renderer.texture;
}
if (obj._renderer.positionBuffer) {
gl.deleteBuffer(obj._renderer.positionBuffer);
delete obj._renderer.positionBuffer;
}
if (obj._renderer.effect) {
obj._renderer.effect = null;
}
}
if (this.type === "CanvasRenderer" && obj._renderer.context) {
delete obj._renderer.context;
}
}
return obj;
}
/**
* @name Two#getShapesAtPoint
* @function
* @param {Number} x - X coordinate in world space.
* @param {Number} y - Y coordinate in world space.
* @param {Object} [options] - Hit test configuration.
* @param {Boolean} [options.visibleOnly=true] - Limit results to visible shapes.
* @param {Boolean} [options.includeGroups=false] - Include groups in the hit results.
* @param {('all'|'deepest')} [options.mode='all'] - Whether to return all intersecting shapes or only the top-most.
* @param {Boolean} [options.deepest] - Alias for `mode: 'deepest'`.
* @param {Number} [options.precision] - Segmentation precision for curved geometry.
* @param {Number} [options.tolerance=0] - Pixel tolerance applied to hit testing.
* @param {Boolean} [options.fill] - Override fill testing behaviour.
* @param {Boolean} [options.stroke] - Override stroke testing behaviour.
* @param {Function} [options.filter] - Predicate to filter shapes from the result set.
* @returns {Two.Shape[]} Ordered list of shapes under the specified point, front to back.
* @description Returns shapes underneath the provided coordinates. Coordinates are expected in world space (matching the renderer output).
* @nota-bene Delegates to {@link Two.Group#getShapesAtPoint} on the root scene.
*/
getShapesAtPoint(x, y, options) {
if (this.scene && typeof this.scene.getShapesAtPoint === "function") {
return this.scene.getShapesAtPoint(x, y, options);
}
return [];
}
/**
* @name Two#update
* @function
* @fires update
* @description Update positions and calculations in one pass before rendering. Then render to the canvas.
* @nota-bene This function is called automatically if using {@link Two#play} or the `autostart` parameter in construction.
*/
update() {
const animated = !!this._lastFrame;
const now = _.performance.now();
if (animated) {
this.timeDelta = parseFloat((now - this._lastFrame).toFixed(3));
}
this._lastFrame = now;
if (this.fit && this.fit.domElement && !this.fit.attached) {
dom.bind(this.fit.domElement, "resize", this.fit);
this.fit.attached = true;
this.fit();
}
const width = this.width;
const height = this.height;
const renderer = this.renderer;
if (width !== renderer.width || height !== renderer.height) {
renderer.setSize(width, height, this.ratio);
}
this.trigger(Events.Types.update, this.frameCount, this.timeDelta);
return this.render();
}
/**
* @name Two#render
* @function
* @fires render
* @description Render all drawable and visible objects of the scene.
*/
render() {
this.renderer.render();
return this.trigger(Events.Types.render, this.frameCount++);
}
// Convenience Methods
/**
* @name Two#add
* @function
* @param {(Two.Shape[]|...Two.Shape)} [objects] - An array of Two.js objects. Alternatively can add objects as individual arguments.
* @description A shorthand method to add specific Two.js objects to the scene.
*/
add(objects) {
if (!(objects instanceof Array)) {
objects = Array.prototype.slice.call(arguments);
}
this.scene.add(objects);
return this;
}
/**
* @name Two#remove
* @function
* @param {(Two.Shape[]|...Two.Shape)} [objects] - An array of Two.js objects.
* @description A shorthand method to remove specific Two.js objects from the scene.
*/
remove(objects) {
if (!(objects instanceof Array)) {
objects = Array.prototype.slice.call(arguments);
}
this.scene.remove(objects);
return this;
}
/**
* @name Two#clear
* @function
* @description Removes all objects from the instance's scene. If you intend to have the browser garbage collect this, don't forget to delete the references in your application as well.
*/
clear() {
this.scene.remove(this.scene.children);
return this;
}
/**
* @name Two#makeLine
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* @returns {Two.Line}
* @description Creates a Two.js line and adds it to the scene.
*/
makeLine(x1, y1, x2, y2) {
const line = new Line(x1, y1, x2, y2);
this.scene.add(line);
return line;
}
/**
* @name Two#makeArrow
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* @returns {Two.Path}
* @description Creates a Two.js arrow and adds it to the scene.
*/
makeArrow(x1, y1, x2, y2, size) {
const headlen = typeof size === "number" ? size : 10;
const angle = Math.atan2(y2 - y1, x2 - x1);
const vertices = [
new Anchor(
x1,
y1,
void 0,
void 0,
void 0,
void 0,
Commands.move
),
new Anchor(
x2,
y2,
void 0,
void 0,
void 0,
void 0,
Commands.line
),
new Anchor(
x2 - headlen * Math.cos(angle - Math.PI / 4),
y2 - headlen * Math.sin(angle - Math.PI / 4),
void 0,
void 0,
void 0,
void 0,
Commands.line
),
new Anchor(
x2,
y2,
void 0,
void 0,
void 0,
void 0,
Commands.move
),
new Anchor(
x2 - headlen * Math.cos(angle + Math.PI / 4),
y2 - headlen * Math.sin(angle + Math.PI / 4),
void 0,
void 0,
void 0,
void 0,
Commands.line
)
];
const path = new Path(vertices, false, false, true);
path.noFill();
path.cap = "round";
path.join = "round";
this.scene.add(path);
return path;
}
/**
* @name Two#makeRectangle
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} width
* @param {Number} height
* @returns {Two.Rectangle}
* @description Creates a Two.js rectangle and adds it to the scene.
*/
makeRectangle(x, y, width, height) {
const rect = new Rectangle(x, y, width, height);
this.scene.add(rect);
return rect;
}
/**
* @name Two#makeRoundedRectangle
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} width
* @param {Number} height
* @param {Number} sides
* @returns {Two.RoundedRectangle}
* @description Creates a Two.js rounded rectangle and adds it to the scene.
*/
makeRoundedRectangle(x, y, width, height, sides) {
const rect = new RoundedRectangle(x, y, width, height, sides);
this.scene.add(rect);
return rect;
}
/**
* @name Two#makeCircle
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} radius
* @param {Number} [resolution=4]
* @returns {Two.Circle}
* @description Creates a Two.js circle and adds it to the scene.
*/
makeCircle(x, y, radius, resolution) {
const circle = new Circle(x, y, radius, resolution);
this.scene.add(circle);
return circle;
}
/**
* @name Two#makeEllipse
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} rx
* @param {Number} ry
* @param {Number} [resolution=4]
* @returns {Two.Ellipse}
* @description Creates a Two.js ellipse and adds it to the scene.
*/
makeEllipse(x, y, rx, ry, resolution) {
const ellipse = new Ellipse(x, y, rx, ry, resolution);
this.scene.add(ellipse);
return ellipse;
}
/**
* @name Two#makeStar
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} outerRadius
* @param {Number} innerRadius
* @param {Number} sides
* @returns {Two.Star}
* @description Creates a Two.js star and adds it to the scene.
*/
makeStar(x, y, outerRadius, innerRadius, sides) {
const star = new Star(x, y, outerRadius, innerRadius, sides);
this.scene.add(star);
return star;
}
/**
* @name Two#makeCurve
* @function
* @param {Two.Anchor[]} [points] - An array of {@link Two.Anchor} points.
* @param {...Number} - Alternatively you can pass alternating `x` / `y` coordinate values as individual arguments. These will be combined into {@link Two.Anchor}s for use in the path.
* @returns {Two.Path} - Where `path.curved` is set to `true`.
* @description Creates a Two.js path that is curved and adds it to the scene.
* @nota-bene In either case of passing an array or passing numbered arguments the last argument is an optional `Boolean` that defines whether the path should be open or closed.
*/
makeCurve(points) {
const l = arguments.length;
if (!Array.isArray(points)) {
points = [];
for (let i = 0; i < l; i += 2) {
const x = arguments[i];
if (typeof x !== "number") {
break;
}
const y = arguments[i + 1];
points.push(new Anchor(x, y));
}
}
const last = arguments[l - 1];
const curve = new Path(
points,
!(typeof last === "boolean" ? last : void 0),
true
);
const rect = curve.getBoundingClientRect();
curve.center().translation.set(rect.left + rect.width / 2, rect.top + rect.height / 2);
this.scene.add(curve);
return curve;
}
/**
* @name Two#makePolygon
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} radius
* @param {Number} sides
* @returns {Two.Polygon}
* @description Creates a Two.js polygon and adds it to the scene.
*/
makePolygon(x, y, radius, sides) {
const poly = new Polygon(x, y, radius, sides);
this.scene.add(poly);
return poly;
}
/**
* @name Two#makeArcSegment
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} innerRadius
* @param {Number} outerRadius
* @param {Number} startAngle
* @param {Number} endAngle
* @param {Number} [resolution=Two.Resolution] - The number of vertices that should comprise the arc segment.
* @returns {Two.ArcSegment}
*/
makeArcSegment(x, y, innerRadius, outerRadius, startAngle, endAngle, resolution) {
const arcSegment = new ArcSegment(
x,
y,
innerRadius,
outerRadius,
startAngle,
endAngle,
resolution
);
this.scene.add(arcSegment);
return arcSegment;
}
/**
* @name Two#makePoints
* @function
* @param {Two.Vector[]} [points] - An array of {@link Two.Vector} points
* @param {...Number} - Alternatively you can pass alternating `x` / `y` coordinate values as individual agrguments. These will be combined into {@link Two.Vector}s for use in the points object.
* @returns {Two.Points}
* @description Creates a Two.js points object and adds it to the current scene.
*/
makePoints(p) {
const l = arguments.length;
let vertices = p;
if (!Array.isArray(p)) {
vertices = [];
for (let i = 0; i < l; i += 2) {
const x = arguments[i];
if (typeof x !== "number") {
break;
}
const y = arguments[i + 1];
vertices.push(new Vector(x, y));
}
}
const points = new Points(vertices);
this.scene.add(points);
return points;
}
/**
* @name Two#makePath
* @function
* @param {Two.Anchor[]} [points] - An array of {@link Two.Anchor} points
* @param {...Number} - Alternatively you can pass alternating `x` / `y` coordinate values as individual arguments. These will be combined into {@link Two.Anchor}s for use in the path.
* @returns {Two.Path}
* @description Creates a Two.js path and adds it to the scene.
* @nota-bene In either case of passing an array or passing numbered arguments the last argument is an optional `Boolean` that defines whether the path should be open or closed.
*/
makePath(p) {
const l = arguments.length;
let points = p;
if (!Array.isArray(p)) {
points = [];
for (let i = 0; i < l; i += 2) {
const x = arguments[i];
if (typeof x !== "number") {
break;
}
const y = arguments[i + 1];
points.push(new Anchor(x, y));
}
}
const last = arguments[l - 1];
const path = new Path(
points,
!(typeof last === "boolean" ? last : void 0)
);
const rect = path.getBoundingClientRect();
if (typeof rect.top === "number" && typeof rect.left === "number" && typeof rect.right === "number" && typeof rect.bottom === "number") {
path.center().translation.set(
rect.left + rect.width / 2,
rect.top + rect.height / 2
);
}
this.scene.add(path);
return path;
}
/**
* @name Two#makeText
* @function
* @param {String} message
* @param {Number} x
* @param {Number} y
* @param {Object} [styles] - An object to describe any of the {@link Two.Text.Properties} including `fill`, `stroke`, `linewidth`, `family`, `alignment`, `leading`, `opacity`, etc..
* @returns {Two.Text}
* @description Creates a Two.js text object and adds it to the scene.
*/
makeText(message, x, y, styles) {
const text = new Text(message, x, y, styles);
this.add(text);
return text;
}
/**
* @name Two#makeLinearGradient
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* @param {...Two.Stop} args - Any number of color stops sometimes referred to as ramp stops. If none are supplied then the default black-to-white two stop gradient is applied.
* @returns {Two.LinearGradient}
* @description Creates a Two.js linear gradient and adds it to the scene. In the case of an effect it's added to an invisible "definitions" group.
*/
makeLinearGradient(x1, y1, x2, y2) {
const stops = Array.prototype.slice.call(arguments, 4);
const gradient = new LinearGradient(x1, y1, x2, y2, stops);
this.add(gradient);
return gradient;
}
/**
* @name Two#makeRadialGradient
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} radius
* @param {...Two.Stop} args - Any number of color stops sometimes referred to as ramp stops. If none are supplied then the default black-to-white two stop gradient is applied.
* @returns {Two.RadialGradient}
* @description Creates a Two.js linear-gradient object and adds it to the scene. In the case of an effect it's added to an invisible "definitions" group.
*/
makeRadialGradient(x1, y1, radius) {
const stops = Array.prototype.slice.call(arguments, 3);
const gradient = new RadialGradient(x1, y1, radius, stops);
this.add(gradient);
return gradient;
}
/**
* @name Two#makeSprite
* @function
* @param {(String|Two.Texture)} src - The URL path to an image or an already created {@link Two.Texture}.
* @param {Number} x
* @param {Number} y
* @param {Number} [columns=1]
* @param {Number} [rows=1]
* @param {Number} [frameRate=0]
* @param {Boolean} [autostart=false]
* @returns {Two.Sprite}
* @description Creates a Two.js sprite object and adds it to the scene. Sprites can be used for still images as well as animations.
*/
makeSprite(src, x, y, columns, rows, frameRate, autostart) {
const sprite = new Sprite(src, x, y, columns, rows, frameRate);
if (autostart) {
sprite.play();
}
this.add(sprite);
return sprite;
}
/**
* @name Two#makeImage
* @function
* @param {(String|Two.Texture)} src - The URL path to an image or an already created {@link Two.Texture}.
* @param {Number} x
* @param {Number} y
* @param {Number} width
* @param {Number} height
* @param {String} [mode="fill"]
* @returns {Two.Image}
* @description Creates a Two.js image object and adds it to the scene. Images are scaled to fit the provided width and height.
*/
makeImage(src, x, y, width, height, mode) {
const image = new Image2(src, x, y, width, height, mode);
this.add(image);
return image;
}
/**
* @name Two#makeImageSequence
* @function
* @param {(String[]|Two.Texture[])} src - An array of paths or of {@link Two.Textures}.
* @param {Number} x
* @param {Number} y
* @param {Number} [frameRate=0]
* @param {Boolean} [autostart=false]
* @returns {Two.ImageSequence}
* @description Creates a Two.js image sequence object and adds it to the scene.
*/
makeImageSequence(src, x, y, frameRate, autostart) {
const imageSequence = new ImageSequence(src, x, y, frameRate);
if (autostart) {
imageSequence.play();
}
this.add(imageSequence);
return imageSequence;
}
/**
* @name Two#makeTexture
* @function
* @param {(String|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement)} [src] - The URL path to an image or a DOM image-like element.
* @param {Function} [callback] - Function to be invoked when the image is loaded.
* @returns {Two.Texture}
* @description Creates a Two.js texture object.
*/
makeTexture(src, callback) {
const texture = new Texture(src, callback);
return texture;
}
/**
* @name Two#makeGroup
* @function
* @param {(Two.Shape[]|...Two.Shape)} [objects] - Two.js objects to be added to the group in the form of an array or as individual arguments.
* @returns {Two.Group}
* @description Creates a Two.js group object and adds it to the scene.
*/
makeGroup(objects) {
if (!(objects instanceof Array)) {
objects = Array.prototype.slice.call(arguments);
}
const group = new Group();
this.scene.add(group);
group.add(objects);
return group;
}
/**
* @name Two#interpret
* @function
* @param {SVGElement} svg - The SVG node to be parsed.
* @param {Boolean} shallow - Don't create a top-most group but append all content directly.
* @param {Boolean} [add=true] – Automatically add the reconstructed SVG node to scene.
* @returns {Two.Group}
* @description Interpret an SVG Node and add it to this instance's scene. The distinction should be made that this doesn't `import` svg's, it solely interprets them into something compatible for Two.js - this is slightly different than a direct transcription.
*/
interpret(svg2, shallow, add) {
const tag = svg2.tagName.toLowerCase();
add = typeof add !== "undefined" ? add : true;
if (!(tag in read)) {
return null;
}
const node = read[tag].call(this, svg2);
if (add) {
this.add(shallow && node instanceof Group ? node.children : node);
} else if (node.parent) {
node.remove();
}
return node;
}
/**
* @name Two#load
* @function
* @param {String|SVGElement} pathOrSVGContent - The URL path of an SVG file or an SVG document as text.
* @param {Function} [callback] - Function to call once loading has completed.
* @returns {Two.Group}
* @description Load an SVG file or SVG text and interpret it into Two.js legible objects.
*/
load(pathOrSVGContent, callback) {
const group = new Group();
let elem, i, child;
const attach = function(data) {
dom.temp.innerHTML = data;
for (i = 0; i < dom.temp.children.length; i++) {
elem = dom.temp.children[i];
child = this.interpret(elem, false, false);
if (child !== null) {
group.add(child);
}
}
if (typeof callback === "function") {
const svg2 = dom.temp.children.length <= 1 ? dom.temp.children[0] : dom.temp.children;
callback(group, svg2);
}
}.bind(this);
if (/\.svg$/i.test(pathOrSVGContent)) {
xhr(pathOrSVGContent, attach);
return group;
}
attach(pathOrSVGContent);
return group;
}
};
__publicField(_Two, "NextFrameId", Constants.NextFrameId);
// Primitive
/**
* @name Two.Types
* @property {Object} - The different rendering types available in the library.
*/
__publicField(_Two, "Types", Constants.Types);
/**
* @name Two.Version
* @property {String} - The current working version of the library, `$version`.
*/
__publicField(_Two, "Version", Constants.Version);
/**
* @name Two.PublishDate
* @property {String} - The automatically generated publish date in the build process to verify version release candidates.
*/
__publicField(_Two, "PublishDate", Constants.PublishDate);
/**
* @name Two.Identifier
* @property {String} - String prefix for all Two.js object's ids. This trickles down to SVG ids.
*/
__publicField(_Two, "Identifier", Constants.Identifier);
/**
* @name Two.Resolution
* @property {Number} - Default amount of vertices to be used for interpreting Arcs and ArcSegments.
*/
__publicField(_Two, "Resolution", Constants.Resolution);
/**
* @name Two.AutoCalculateImportedMatrices
* @property {Boolean} - When importing SVGs through the {@link Two#interpret} and {@link Two#load}, this boolean determines whether Two.js infers and then overrides the exact transformation matrix of the reference SVG.
* @nota-bene `false` copies the exact transformation matrix values, but also sets the path's `matrix.manual = true`.
*/
__publicField(_Two, "AutoCalculateImportedMatrices", Constants.AutoCalculateImportedMatrices);
/**
* @name Two.Instances
* @property {Two[]} - Registered list of all Two.js instances in the current session.
*/
__publicField(_Two, "Instances", Constants.Instances);
/**
* @function Two.uniqueId
* @description Simple method to access an incrementing value. Used for `id` allocation on all Two.js objects.
* @returns {Number} Ever increasing Number.
*/
__publicField(_Two, "uniqueId", Constants.uniqueId);
__publicField(_Two, "Anchor", Anchor);
__publicField(_Two, "Collection", Collection);
__publicField(_Two, "Events", Events);
__publicField(_Two, "Group", Group);
__publicField(_Two, "Matrix", Matrix2);
__publicField(_Two, "Path", Path);
__publicField(_Two, "Registry", Registry);
__publicField(_Two, "Element", Element);
__publicField(_Two, "Shape", Shape);
__publicField(_Two, "Text", Text);
__publicField(_Two, "Vector", Vector);
__publicField(_Two, "Gradient", Gradient);
__publicField(_Two, "Image", Image2);
__publicField(_Two, "ImageSequence", ImageSequence);
__publicField(_Two, "LinearGradient", LinearGradient);
__publicField(_Two, "RadialGradient", RadialGradient);
__publicField(_Two, "Sprite", Sprite);
__publicField(_Two, "Stop", Stop);
__publicField(_Two, "Texture", Texture);
__publicField(_Two, "ArcSegment", ArcSegment);
__publicField(_Two, "Circle", Circle);
__publicField(_Two, "Ellipse", Ellipse);
__publicField(_Two, "Line", Line);
__publicField(_Two, "Points", Points);
__publicField(_Two, "Polygon", Polygon);
__publicField(_Two, "Rectangle", Rectangle);
__publicField(_Two, "RoundedRectangle", RoundedRectangle);
__publicField(_Two, "Star", Star);
__publicField(_Two, "CanvasRenderer", Renderer);
__publicField(_Two, "SVGRenderer", Renderer2);
__publicField(_Two, "WebGLRenderer", Renderer3);
/**
* @name Two.Commands
* @property {Object} - Map of possible path commands. Taken from the SVG specification. Commands include: `move`, `line`, `curve`, `arc`, and `close`.
*/
__publicField(_Two, "Commands", Commands);
/**
* @name Two.Utils
* @property {Object} Utils - A massive object filled with utility functions and properties.
* @property {Object} Two.Utils.read - A collection of SVG parsing functions indexed by element name.
* @property {Function} Two.Utils.read.path - Parse SVG path element or `d` attribute string.
*/
__publicField(_Two, "Utils", Utils);
var Two = _Two;
function fitToWindow() {
const wr = document.body.getBoundingClientRect();
const width = this.width = wr.width;
const height = this.height = wr.height;
this.renderer.setSize(width, height, this.ratio);
}
function fitToParent() {
const parent = this.renderer.domElement.parentElement;
if (!parent) {
console.warn("Two.js: Attempting to fit to parent, but no parent found.");
return;
}
const wr = parent.getBoundingClientRect();
const width = this.width = wr.width;
const height = this.height = wr.height;
this.renderer.setSize(width, height, this.ratio);
}
function updateDimensions(width, height) {
this.width = width;
this.height = height;
this.trigger(Events.Types.resize, width, height);
}
var raf = dom.getRequestAnimationFrame();
function loop() {
for (let i = 0; i < Two.Instances.length; i++) {
const t = Two.Instances[i];
if (t.playing) {
t.update();
}
}
Two.NextFrameId = raf(loop);
}
raf.init = function() {
loop();
raf.init = function() {
};
};
export {
Two as default
};
================================================
FILE: deploy.sh
================================================
#!/usr/bin/env sh
# abort on errors
set -e
# build
npm run docs:build
# navigate into the build output directory
cd wiki/.vuepress/dist
# if you are deploying to a custom domain
echo 'two.js.org' > CNAME
git init
git add -A
git commit -m 'deploy'
git push -f git@github.com:jonobr1/two.js.git main:gh-pages
cd -
================================================
FILE: extras/js/arc.js
================================================
(function () {
const TWO_PI = Math.PI * 2;
const cos = Math.cos,
sin = Math.sin;
/**
* @name Two.Arc
* @class
* @extends Two.Path
* @param {Number} [x=0] - The x position of the arc.
* @param {Number} [y=0] - The y position of the arc.
* @param {Number} [width=0] - The width, horizontal diameter, of the arc.
* @param {Number} [height=0] - The height, vertical diameter, of the arc.
* @param {Number} [startAngle=0] - The starting angle of the arc in radians.
* @param {Number} [endAngle=6.283] - The ending angle of the arc in radians.
* @param {Number} [resolution=4] - The number of vertices used to construct the circle.
*/
class Arc extends Two.Path {
_flagWidth = false;
_flagHeight = false;
_flagStartAngle = false;
_flagEndAngle = false;
_width = 0;
_height = 0;
_startAngle = 0;
_endAngle = TWO_PI;
constructor(x, y, width, height, startAngle, endAngle, resolution) {
if (typeof resolution !== 'number') {
resolution = Two.Resolution;
}
const points = [];
for (let i = 0; i < resolution; i++) {
points.push(new Two.Anchor());
}
super(points);
for (let j = 0; j < Arc.Properties.length; j++) {
const prop = Arc.Properties[j];
Object.defineProperty(this, prop, protos[prop]);
}
this.curved = true;
/**
* @name Two.Arc#width
* @property {Number} - The horizontal size of the arc.
*/
if (typeof width === 'number') {
this.width = width;
}
/**
* @name Two.Arc#height
* @property {Number} - The vertical size of the arc.
*/
if (typeof height === 'number') {
this.height = height;
}
/**
* @name Two.ArcSegment#startAngle
* @property {Number} - The angle of one side for the arc segment.
*/
if (typeof startAngle === 'number') {
this.startAngle = startAngle;
}
/**
* @name Two.ArcSegment#endAngle
* @property {Number} - The angle of the other side for the arc segment.
*/
if (typeof endAngle === 'number') {
this.endAngle = endAngle;
}
this._update();
if (typeof x === 'number') {
this.position.x = x;
}
if (typeof y === 'number') {
this.position.y = y;
}
}
/**
* @name Two.Arc.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Arc}.
*/
static Properties = ['width', 'height', 'startAngle', 'endAngle'];
/**
* @name Two.Arc#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (
this._flagVertices ||
this._flagRadius ||
this._flagStartAngle ||
this._flagWidth ||
this._flagHeight ||
this._flagEndAngle
) {
const { width, height, startAngle, endAngle, vertices } = this;
const rx = width / 2;
const ry = height / 2;
for (let i = 0; i < vertices.length; i++) {
const v = vertices[i];
const pct = i / (vertices.length - 1);
const theta = pct * (endAngle - startAngle) + startAngle;
v.x = rx * cos(theta);
v.y = ry * sin(theta);
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Arc#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
super.flagReset.call(this);
this._flagWidth =
this._flagHeight =
this._flagStartAngle =
this._flagEndAngle =
false;
return this;
}
/**
* @name Two.Arc#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.ArcSegment}
* @description Create a new instance of {@link Two.ArcSegment} with the same properties of the current path.
*/
clone() {
const { width, height, startAngle, endAngle } = this;
const resolution = this.vertices.length;
const clone = new Arc(
0,
0,
width,
height,
startAngle,
endAngle,
resolution
);
clone.position.copy(this.position);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Two.Path.Properties.length; i++) {
const k = Two.Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
}
const protos = {
width: {
enumerable: true,
get: function () {
return this._width;
},
set: function (v) {
if (v !== this._width) {
this._width = v;
this._flagWidth = true;
}
},
},
height: {
enumerable: true,
get: function () {
return this._height;
},
set: function (v) {
if (v !== this._height) {
this._height = v;
this._flagHeight = true;
}
},
},
startAngle: {
enumerable: true,
get: function () {
return this._startAngle;
},
set: function (v) {
if (v !== this._startAngle) {
this._startAngle = v;
this._flagStartAngle = true;
}
},
},
endAngle: {
enumerable: true,
get: function () {
return this._endAngle;
},
set: function (v) {
if (v !== this._endAngle) {
this._endAngle = v;
this._flagEndAngle = true;
}
},
},
};
Two.Arc = Arc;
})();
================================================
FILE: extras/js/zui.js
================================================
(function () {
class Surface {
constructor(object) {
this.object = object;
}
limits(min, max) {
const min_exists = typeof min !== 'undefined';
const max_exists = typeof max !== 'undefined';
if (!max_exists && !min_exists) {
return { min: this.min, max: this.max };
}
this.min = min_exists ? min : this.min;
this.max = max_exists ? max : this.max;
return this;
}
apply(px, py, s) {
this.object.translation.set(px, py);
this.object.scale = s;
return this;
}
}
/**
* @name Two.ZUI
* @class
* @param {Two.Group} group - The scene or group to
* @param {HTMLElement} [domElement=document.body] - The HTML Element to attach event listeners to.
*/
class ZUI {
constructor(group, domElement) {
this.limits = {
scale: ZUI.Limit.clone(),
x: ZUI.Limit.clone(),
y: ZUI.Limit.clone(),
};
this.viewport = domElement || document.body;
this.viewportOffset = {
top: 0,
left: 0,
matrix: new Two.Matrix(),
};
this.surfaceMatrix = new Two.Matrix();
this.surfaces = [];
this.reset();
this.updateSurface();
this.add(new Surface(group));
}
static Surface = Surface;
static Clamp(v, min, max) {
return Math.min(Math.max(v, min), max);
}
static Limit = {
min: -Infinity,
max: Infinity,
clone: function () {
const result = {};
for (let k in this) {
result[k] = this[k];
}
return result;
},
};
static TranslateMatrix(m, x, y) {
m.elements[2] += x;
m.elements[5] += y;
return m;
}
static PositionToScale(pos) {
return Math.exp(pos);
}
static ScaleToPosition(scale) {
return Math.log(scale);
}
//
add(surface) {
this.surfaces.push(surface);
const limits = surface.limits();
this.addLimits(limits.min, limits.max);
return this;
}
addLimits(min, max) {
if (typeof min !== 'undefined') {
if (this.limits.scale.min) {
this.limits.scale.min = Math.max(min, this.limits.scale.min);
} else {
this.limits.scale.min = min;
}
}
if (typeof max === 'undefined') {
return this;
}
if (this.limits.scale.max) {
this.limits.scale.max = Math.min(max, this.limits.scale.max);
} else {
this.limits.scale.max = max;
}
return this;
}
clientToSurface(a, b, c) {
this.updateOffset();
const m = this.surfaceMatrix.inverse();
let x, y, z;
if (arguments.length === 1) {
const v = a;
x = typeof v.x === 'number' ? v.x : 0;
y = typeof v.y === 'number' ? v.y : 0;
z = typeof v.z === 'number' ? v.z : 1;
} else {
x = typeof a === 'number' ? a : 0;
y = typeof b === 'number' ? b : 0;
z = typeof c === 'number' ? c : 1;
}
const n = this.viewportOffset.matrix.inverse().multiply(x, y, z);
const r = m.multiply(n[0], n[1], n[2]);
return { x: r[0], y: r[1], z: r[2] };
}
surfaceToClient(a, b, c) {
this.updateOffset();
const vo = this.viewportOffset.matrix.clone();
let x, y, z;
if (arguments.length === 1) {
const v = a;
x = typeof v.x === 'number' ? v.x : 0;
y = typeof v.y === 'number' ? v.y : 0;
z = typeof v.z === 'number' ? v.z : 1;
} else {
x = typeof a === 'number' ? a : 0;
y = typeof b === 'number' ? b : 0;
z = typeof c === 'number' ? c : 1;
}
const sm = this.surfaceMatrix.multiply(x, y, z);
const r = vo.multiply(sm[0], sm[1], sm[2]);
return { x: r[0], y: r[1], z: r[2] };
}
zoomBy(byF, clientX, clientY) {
const s = ZUI.PositionToScale(this.zoom + byF);
this.zoomSet(s, clientX, clientY);
return this;
}
zoomSet(zoom, clientX, clientY) {
const newScale = this.fitToLimits(zoom);
this.zoom = ZUI.ScaleToPosition(newScale);
if (newScale === this.scale) {
return this;
}
const sf = this.clientToSurface(clientX, clientY);
const scaleBy = newScale / this.scale;
this.surfaceMatrix.scale(scaleBy);
this.scale = newScale;
const c = this.surfaceToClient(sf);
const dx = clientX - c.x;
const dy = clientY - c.y;
this.translateSurface(dx, dy);
return this;
}
translateSurface(x, y) {
ZUI.TranslateMatrix(this.surfaceMatrix, x, y);
this.updateSurface();
return this;
}
updateOffset() {
const rect = this.viewport.getBoundingClientRect();
this.viewportOffset.left = rect.left - document.body.scrollLeft;
this.viewportOffset.top = rect.top - document.body.scrollTop;
this.viewportOffset.matrix
.identity()
.translate(this.viewportOffset.left, this.viewportOffset.top);
return this;
}
updateSurface() {
const e = this.surfaceMatrix.elements;
for (let i = 0; i < this.surfaces.length; i++) {
this.surfaces[i].apply(e[2], e[5], e[0]);
}
return this;
}
reset() {
this.zoom = 0;
this.scale = 1.0;
this.surfaceMatrix.identity();
this.updateSurface();
return this;
}
fitToLimits(s) {
return ZUI.Clamp(s, this.limits.scale.min, this.limits.scale.max);
}
}
Two.ZUI = ZUI;
})();
================================================
FILE: extras/jsm/arc.js
================================================
import { TWO_PI } from '../../src/utils/math.js';
import { Constants } from '../../src/constants.js';
import { Anchor } from '../../src/anchor.js';
import { Path } from '../../src/path.js';
const cos = Math.cos,
sin = Math.sin;
/**
* @name Two.Arc
* @class
* @extends Two.Path
* @param {Number} [x=0] - The x position of the arc.
* @param {Number} [y=0] - The y position of the arc.
* @param {Number} [width=0] - The width, horizontal diameter, of the arc.
* @param {Number} [height=0] - The height, vertical diameter, of the arc.
* @param {Number} [startAngle=0] - The starting angle of the arc in radians.
* @param {Number} [endAngle=6.283] - The ending angle of the arc in radians.
* @param {Number} [resolution=4] - The number of vertices used to construct the circle.
*/
export class Arc extends Path {
_flagWidth = false;
_flagHeight = false;
_flagStartAngle = false;
_flagEndAngle = false;
_width = 0;
_height = 0;
_startAngle = 0;
_endAngle = TWO_PI;
constructor(x, y, width, height, startAngle, endAngle, resolution) {
if (typeof resolution !== 'number') {
resolution = Constants.Resolution;
}
const points = [];
for (let i = 0; i < resolution; i++) {
points.push(new Anchor());
}
super(points);
for (let j = 0; j < Arc.Properties.length; j++) {
const prop = Arc.Properties[j];
Object.defineProperty(this, prop, protos[prop]);
}
this.curved = true;
/**
* @name Two.Arc#width
* @property {Number} - The horizontal size of the arc.
*/
if (typeof width === 'number') {
this.width = width;
}
/**
* @name Two.Arc#height
* @property {Number} - The vertical size of the arc.
*/
if (typeof height === 'number') {
this.height = height;
}
/**
* @name Two.ArcSegment#startAngle
* @property {Number} - The angle of one side for the arc segment.
*/
if (typeof startAngle === 'number') {
this.startAngle = startAngle;
}
/**
* @name Two.ArcSegment#endAngle
* @property {Number} - The angle of the other side for the arc segment.
*/
if (typeof endAngle === 'number') {
this.endAngle = endAngle;
}
this._update();
if (typeof x === 'number') {
this.position.x = x;
}
if (typeof y === 'number') {
this.position.y = y;
}
}
/**
* @name Two.Arc.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Arc}.
*/
static Properties = ['width', 'height', 'startAngle', 'endAngle'];
/**
* @name Two.Arc#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (
this._flagVertices ||
this._flagRadius ||
this._flagWidth ||
this._flagHeight ||
this._flagStartAngle ||
this._flagEndAngle
) {
const { width, height, startAngle, endAngle, vertices } = this;
const rx = width / 2;
const ry = height / 2;
for (let i = 0; i < vertices.length; i++) {
const v = vertices[i];
const pct = i / (vertices.length - 1);
const theta = pct * (endAngle - startAngle) + startAngle;
v.x = rx * cos(theta);
v.y = ry * sin(theta);
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Arc#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
super.flagReset.call(this);
this._flagWidth =
this._flagHeight =
this._flagStartAngle =
this._flagEndAngle =
false;
return this;
}
/**
* @name Two.Arc#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.ArcSegment}
* @description Create a new instance of {@link Two.ArcSegment} with the same properties of the current path.
*/
clone() {
const { width, height, startAngle, endAngle } = this;
const resolution = this.vertices.length;
const clone = new Arc(
0,
0,
width,
height,
startAngle,
endAngle,
resolution
);
clone.position.copy(this.position);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
}
const protos = {
width: {
enumerable: true,
get: function () {
return this._width;
},
set: function (v) {
if (v !== this._width) {
this._width = v;
this._flagWidth = true;
}
},
},
height: {
enumerable: true,
get: function () {
return this._height;
},
set: function (v) {
if (v !== this._height) {
this._height = v;
this._flagHeight = true;
}
},
},
startAngle: {
enumerable: true,
get: function () {
return this._startAngle;
},
set: function (v) {
if (v !== this._startAngle) {
this._startAngle = v;
this._flagStartAngle = true;
}
},
},
endAngle: {
enumerable: true,
get: function () {
return this._endAngle;
},
set: function (v) {
if (v !== this._endAngle) {
this._endAngle = v;
this._flagEndAngle = true;
}
},
},
};
================================================
FILE: extras/jsm/zui.d.ts
================================================
declare module 'two.js/extras/jsm/zui' {
/**
* @name Two.ZUI
* @class
* @param {Group} group - The scene or group to
* @param {HTMLElement} [domElement=document.body] - The HTML Element to attach event listeners to.
*/
export class ZUI {
static Surface: Surface;
static Clamp(v: any, min: any, max: any): number;
static Limit: {
min: number;
max: number;
clone: () => {};
};
static TranslateMatrix(m: any, x: any, y: any): any;
static PositionToScale(pos: any): number;
static ScaleToPosition(scale: any): number;
constructor(group?: Group, domElement?: HTMLElement);
limits: {
scale: {};
x: {};
y: {};
};
viewport: any;
viewportOffset: {
top: number;
left: number;
matrix: Matrix;
};
surfaceMatrix: Matrix;
surfaces: any[];
add(surface: any): ZUI;
addLimits(min: number, max: number, type?: number): ZUI;
clientToSurface(v?: { x?: number; y?: number; z?: number }): {
x: number;
y: number;
z: number;
};
surfaceToClient(v?: { x?: number; y?: number; z?: number }): {
x: number;
y: number;
z: number;
};
zoomBy(byF: any, clientX: any, clientY: any): ZUI;
zoomSet(zoom: any, clientX: any, clientY: any): ZUI;
zoom: number;
scale: any;
translateSurface(x: any, y: any): ZUI;
updateOffset(): ZUI;
updateSurface(): ZUI;
reset(): ZUI;
fitToLimits(s: any): number;
}
import { Matrix } from 'two.js/src/matrix';
import { Group } from 'two.js/src/group';
class Surface {
constructor(object: any);
object: any;
limits(
min: any,
max: any
):
| Surface
| {
min: any;
max: any;
};
min: any;
max: any;
apply(px: any, py: any, s: any): Surface;
}
}
================================================
FILE: extras/jsm/zui.js
================================================
import { Matrix } from '../../src/matrix.js';
class Surface {
constructor(object) {
this.object = object;
}
limits(min, max) {
const min_exists = typeof min !== 'undefined';
const max_exists = typeof max !== 'undefined';
if (!max_exists && !min_exists) {
return { min: this.min, max: this.max };
}
this.min = min_exists ? min : this.min;
this.max = max_exists ? max : this.max;
return this;
}
apply(px, py, s) {
this.object.translation.set(px, py);
this.object.scale = s;
return this;
}
}
/**
* @name Two.ZUI
* @class
* @param {Two.Group} group - The scene or group to
* @param {HTMLElement} [domElement=document.body] - The HTML Element to attach event listeners to.
* @description {@link Two.ZUI} is an extra class to turn your Two.js scene into a Google Maps or Adobe Illustrator style interface. See {@link https://codepen.io/jonobr1/pen/PobMKwb} for example usage.
*/
export class ZUI {
constructor(group, domElement) {
this.limits = {
scale: ZUI.Limit.clone(),
x: ZUI.Limit.clone(),
y: ZUI.Limit.clone(),
};
this.viewport = domElement || document.body;
this.viewportOffset = {
top: 0,
left: 0,
matrix: new Matrix(),
};
this.surfaceMatrix = new Matrix();
this.surfaces = [];
this.reset();
this.updateSurface();
this.add(new Surface(group));
}
static Surface = Surface;
static Clamp(v, min, max) {
return Math.min(Math.max(v, min), max);
}
static Limit = {
min: -Infinity,
max: Infinity,
clone: function () {
const result = {};
for (let k in this) {
result[k] = this[k];
}
return result;
},
};
static TranslateMatrix(m, x, y) {
m.elements[2] += x;
m.elements[5] += y;
return m;
}
static PositionToScale(pos) {
return Math.exp(pos);
}
static ScaleToPosition(scale) {
return Math.log(scale);
}
//
add(surface) {
this.surfaces.push(surface);
const limits = surface.limits();
this.addLimits(limits.min, limits.max);
return this;
}
/**
* @name Two.ZUI#addLimits
* @function
* @param {Number} [min=-Infinity] - The minimum scale the ZUI can zoom out to.
* @param {Number} [max=Infinity] - The maximum scale the ZUI can zoom in to.
*/
addLimits(min, max) {
if (typeof min !== 'undefined') {
if (this.limits.scale.min) {
this.limits.scale.min = Math.max(min, this.limits.scale.min);
} else {
this.limits.scale.min = min;
}
}
if (typeof max === 'undefined') {
return this;
}
if (this.limits.scale.max) {
this.limits.scale.max = Math.min(max, this.limits.scale.max);
} else {
this.limits.scale.max = max;
}
return this;
}
/**
* @name Two.ZUI#clientToSurface
* @function
* @param {Two.Vector} a
* @description Convert an x, y coordinate in the user’s space to the object's projected space. Optionally pass a z property on the object to apply depth.
* @returns {Object} - An object with x, y, and z components
* @overloaded
*/
/**
* @name Two.ZUI#clientToSurface
* @param {Number} [a=0] - The x component of position to be transformed.
* @param {Number} [b=0] - The y component of position to be transformed.
* @param {Number} [c=1] - The optional z component of position to be transformed.
* @description Convert an x, y coordinate in the user’s space to the object's projected space. Optionally pass a z property on the object to apply depth.
* @returns {Object} - An object with x, y, and z components
* @overloaded
*/
clientToSurface(a, b, c) {
this.updateOffset();
const m = this.surfaceMatrix.inverse();
let x, y, z;
if (arguments.length === 1) {
const v = a;
x = typeof v.x === 'number' ? v.x : 0;
y = typeof v.y === 'number' ? v.y : 0;
z = typeof v.z === 'number' ? v.z : 1;
} else {
x = typeof a === 'number' ? a : 0;
y = typeof b === 'number' ? b : 0;
z = typeof c === 'number' ? c : 1;
}
const n = this.viewportOffset.matrix.inverse().multiply(x, y, z);
const r = m.multiply(n[0], n[1], n[2]);
return { x: r[0], y: r[1], z: r[2] };
}
/**
* @name Two.ZUI#surfaceToClient
* @function
* @param {Two.Vector} a
* @description Convert an x, y coordinate in projected space to the user’s space. Optionally pass a z property on the object to apply depth.
* @returns {Object} - An object with x, y, and z components
* @overloaded
*/
/**
* @name Two.ZUI#surfaceToClient
* @param {Number} [a=0] - The x component of position to be transformed.
* @param {Number} [b=0] - The y component of position to be transformed.
* @param {Number} [c=1] - The optional z component of position to be transformed.
* @description Convert an x, y coordinate in projected space to the user’s space. Optionally pass a z property on the object to apply depth.
* @returns {Object} - An object with x, y, and z components
* @overloaded
*/
surfaceToClient(a, b, c) {
this.updateOffset();
const vo = this.viewportOffset.matrix.clone();
let x, y, z;
if (arguments.length === 1) {
const v = a;
x = typeof v.x === 'number' ? v.x : 0;
y = typeof v.y === 'number' ? v.y : 0;
z = typeof v.z === 'number' ? v.z : 1;
} else {
x = typeof a === 'number' ? a : 0;
y = typeof b === 'number' ? b : 0;
z = typeof c === 'number' ? c : 1;
}
const sm = this.surfaceMatrix.multiply(x, y, z);
const r = vo.multiply(sm[0], sm[1], sm[2]);
return { x: r[0], y: r[1], z: r[2] };
}
/**
* @name Two.ZUI#zoomBy
* @function
* @param {Number} byF - The factor to scale by.
* @param {Number} clientX - The x position of the user's input.
* @param {Number} clientY - The y position of the user's input.
* @description A function to zoom by an incremental amount and a position. Typically used for pinch-and-zoom or mousewheel effects.
*/
zoomBy(byF, clientX, clientY) {
const s = ZUI.PositionToScale(this.zoom + byF);
this.zoomSet(s, clientX, clientY);
return this;
}
/**
* @name Two.ZUI#zoomSet
* @function
* @param {Number} zoom - The level of the zoom.
* @param {Number} clientX - The x position of the user's input.
* @param {Number} clientY - The y position of the user's input.
* @description A function to set the zoom amount and the origin position. This is used internally by {@Two.ZUI#zoomBy}.
*/
zoomSet(zoom, clientX, clientY) {
const newScale = this.fitToLimits(zoom);
this.zoom = ZUI.ScaleToPosition(newScale);
if (newScale === this.scale) {
return this;
}
const sf = this.clientToSurface(clientX, clientY);
const scaleBy = newScale / this.scale;
this.surfaceMatrix.scale(scaleBy);
this.scale = newScale;
const c = this.surfaceToClient(sf);
const dx = clientX - c.x;
const dy = clientY - c.y;
this.translateSurface(dx, dy);
return this;
}
/**
* @name Two.ZUI#translateSurface
* @function
* @param {Number} x - The x amount to pan.
* @param {Number} y - The y amount to pan.
* @description Set the position of the ZUI by an incremental translation amount.
*/
translateSurface(x, y) {
ZUI.TranslateMatrix(this.surfaceMatrix, x, y);
this.updateSurface();
return this;
}
updateOffset() {
const rect = this.viewport.getBoundingClientRect();
this.viewportOffset.left = rect.left - document.body.scrollLeft;
this.viewportOffset.top = rect.top - document.body.scrollTop;
this.viewportOffset.matrix
.identity()
.translate(this.viewportOffset.left, this.viewportOffset.top);
return this;
}
updateSurface() {
const e = this.surfaceMatrix.elements;
for (let i = 0; i < this.surfaces.length; i++) {
this.surfaces[i].apply(e[2], e[5], e[0]);
}
return this;
}
/**
* @name Two.ZUI#reset
* @function
* @description Reset the zoom and scale factors to their original instantiated state.
*/
reset() {
this.zoom = 0;
this.scale = 1.0;
this.surfaceMatrix.identity();
this.updateSurface();
return this;
}
fitToLimits(s) {
return ZUI.Clamp(s, this.limits.scale.min, this.limits.scale.max);
}
}
================================================
FILE: package.json
================================================
{
"name": "two.js",
"version": "v0.8.23",
"description": "A renderer agnostic two-dimensional drawing api for the web.",
"module": "build/two.module.js",
"types": "src/two.d.ts",
"files": [
"package.json",
"LICENSE",
"README.md",
"build",
"extras",
"src"
],
"scripts": {
"build": "node ./utils/build",
"dev": "esbuild --servedir=. --serve=8080",
"docs:generate": "node ./utils/document",
"docs:dev": "vuepress dev wiki",
"docs:build": "vuepress build wiki",
"docs:publish": "./deploy.sh",
"lint": "eslint . --fix"
},
"directories": {
"docs": "wiki/docs",
"test": "tests",
"example": "wiki/examples"
},
"keywords": [
"svg",
"canvas2d",
"webgl",
"animation",
"rendering",
"scenegraph",
"motiongraphics",
"visualization",
"dom",
"w3c",
"bitmap",
"vector"
],
"homepage": "https://two.js.org/",
"author": {
"name": "jonobr1",
"url": "http://jono.fyi/"
},
"main": "build/two.js",
"repository": {
"type": "git",
"url": "https://github.com/jonobr1/two.js"
},
"bugs": {
"url": "https://github.com/jonobr1/two.js/issues"
},
"license": "MIT",
"devDependencies": {
"@types/minimatch": "^6.0.0",
"@vuepress/plugin-google-analytics": "^1.8.2",
"@vuepress/plugin-nprogress": "^1.5.4",
"babel-eslint": "^10.1.0",
"chalk": "^2.4.2",
"commander": "^2.20.3",
"esbuild": "^0.25.9",
"eslint": "^7.8.1",
"esm": "^3.2.25",
"gzip-size": "^5.1.1",
"jsdoc": "^3.6.6",
"jsdoc-api": "^6.0.0",
"typescript": "^4.5.4",
"vuepress": "^1.9.5",
"vuepress-plugin-sitemap": "^2.3.1"
},
"autoupdate": {
"source": "git",
"target": "git://github.com/jonobr1/two.js.git",
"fileMap": [
{
"basePath": "build",
"files": [
"two*.js"
]
}
]
}
}
================================================
FILE: src/anchor.d.ts
================================================
declare module 'two.js/src/anchor' {
/**
* @class
* @name Two.Anchor
* @param {Number} [x=0] - The x position of the root anchor point.
* @param {Number} [y=0] - The y position of the root anchor point.
* @param {Number} [ax=0] - The x position of the left handle point.
* @param {Number} [ay=0] - The y position of the left handle point.
* @param {Number} [bx=0] - The x position of the right handle point.
* @param {Number} [by=0] - The y position of the right handle point.
* @param {String} [command=Two.Commands.move] - The command to describe how to render. Applicable commands are {@link Two.Commands}
* @description An object that holds 3 {@link Two.Vector}s, the anchor point and its corresponding handles: `left` and `right`. In order to properly describe the bezier curve about the point there is also a command property to describe what type of drawing should occur when Two.js renders the anchors.
*/
export class Anchor extends Vector {
static makeBroadcast(scope: Anchor): () => void;
/**
* @name Two.Anchor.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Anchor} to create a new instance
* @returns {Two.Anchor}
* @description Create a new {@link Two.Anchor} from an object notation of a {@link Two.Anchor}.
* @nota-bene Works in conjunction with {@link Two.Anchor#toObject}
*/
static fromObject(
obj:
| object
| {
x?: number;
y?: number;
command?: Commands[keyof Commands];
relative?: boolean;
controls?: {
left: { x: number; y: number } | Vector;
right: { x: number; y: number } | Vector;
};
rx?: number;
ry?: number;
xAxisRotation?: number;
largeArcFlag?: number;
}
): Anchor;
constructor(
x?: number,
y?: number,
ax?: number,
ay?: number,
bx?: number,
by?: number,
command?: Commands[keyof Commands]
);
controls: {
left: Vector;
right: Vector;
};
command: Commands[keyof Commands];
relative: boolean;
rx?: number;
ry?: number;
xAxisRotation?: number;
largeArcFlag?: number;
sweepFlag?: number;
/**
* @name Two.Anchor#copy
* @function
* @param {Two.Anchor} v - The anchor to apply values to.
* @description Copy the properties of one {@link Two.Anchor} onto another.
*/
copy(anchor: Anchor): Anchor;
/**
* @name Two.Anchor#clone
* @function
* @returns {Two.Anchor}
* @description Create a new {@link Two.Anchor}, set all its values to the current instance and return it for use.
*/
clone(): Anchor;
/**
* @name Two.Anchor#toObject
* @function
* @returns {Object} - An object with properties filled out to mirror {@link Two.Anchor}.
* @description Create a JSON compatible plain object of the current instance. Intended for use with storing values in a database.
* @nota-bene Works in conjunction with {@link Two.Anchor.fromObject}
*/
toObject(): object;
/**
* @name Two.Anchor#toString
* @function
* @returns {String} - A String with comma-separated values reflecting the various values on the current instance.
* @description Create a string form of the current instance. Intended for use with storing values in a database. This is lighter to store than the JSON compatible {@link Two.Anchor#toObject}.
*/
toString(): string;
}
import { Vector } from 'two.js/src/vector';
import { Commands } from 'two.js/src/utils/path-commands';
}
================================================
FILE: src/anchor.js
================================================
import { Commands } from './utils/path-commands.js';
import { Events } from './events.js';
import { Vector } from './vector.js';
import { toFixed } from './utils/math.js';
/**
* @class
* @name Two.Anchor
* @param {Number} [x=0] - The x position of the root anchor point.
* @param {Number} [y=0] - The y position of the root anchor point.
* @param {Number} [ax=0] - The x position of the left handle point.
* @param {Number} [ay=0] - The y position of the left handle point.
* @param {Number} [bx=0] - The x position of the right handle point.
* @param {Number} [by=0] - The y position of the right handle point.
* @param {String} [command=Two.Commands.move] - The command to describe how to render. Applicable commands are {@link Two.Commands}
* @extends Two.Vector
* @description An object that holds 3 {@link Two.Vector}s, the anchor point and its corresponding handles: `left` and `right`. In order to properly describe the bezier curve about the point there is also a command property to describe what type of drawing should occur when Two.js renders the anchors.
*/
export class Anchor extends Vector {
controls = {
left: new Vector(),
right: new Vector(),
};
_command = Commands.move;
_relative = true;
_rx = 0;
_ry = 0;
_xAxisRotation = 0;
_largeArcFlag = 0;
_sweepFlag = 1;
constructor(
x = 0,
y = 0,
ax = 0,
ay = 0,
bx = 0,
by = 0,
command = Commands.move
) {
super(x, y);
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this.command = command;
this.relative = true;
const broadcast = Anchor.makeBroadcast(this);
this.controls.left
.set(ax, ay)
.addEventListener(Events.Types.change, broadcast);
this.controls.right
.set(bx, by)
.addEventListener(Events.Types.change, broadcast);
}
static makeBroadcast(scope) {
return broadcast;
function broadcast() {
if (scope._bound) {
scope.dispatchEvent(Events.Types.change);
}
}
}
/**
* @name Two.Anchor.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Anchor} to create a new instance
* @returns {Two.Anchor}
* @description Create a new {@link Two.Anchor} from an object notation of a {@link Two.Anchor}.
* @nota-bene Works in conjunction with {@link Two.Anchor#toObject}
*/
static fromObject(obj) {
return new Anchor().copy(obj);
}
/**
* @name Two.Anchor#copy
* @function
* @param {Two.Anchor} v - The anchor to apply values to.
* @description Copy the properties of one {@link Two.Anchor} onto another.
*/
copy(v) {
this.x = v.x;
this.y = v.y;
if (typeof v.command === 'string') {
this.command = v.command;
}
if (v.controls) {
if (v.controls.left) {
this.controls.left.copy(v.controls.left);
}
if (v.controls.right) {
this.controls.right.copy(v.controls.right);
}
}
if (typeof v.relative === 'boolean') {
this.relative = v.relative;
}
if (typeof v.rx === 'number') {
this.rx = v.rx;
}
if (typeof v.ry === 'number') {
this.ry = v.ry;
}
if (typeof v.xAxisRotation === 'number') {
this.xAxisRotation = v.xAxisRotation;
}
if (typeof v.largeArcFlag === 'number') {
this.largeArcFlag = v.largeArcFlag;
}
if (typeof v.sweepFlag === 'number') {
this.sweepFlag = v.sweepFlag;
}
return this;
}
/**
* @name Two.Anchor#clone
* @function
* @returns {Two.Anchor}
* @description Create a new {@link Two.Anchor}, set all its values to the current instance and return it for use.
*/
clone() {
return new Anchor().copy(this);
}
/**
* @name Two.Anchor#toObject
* @function
* @returns {Object} - An object with properties filled out to mirror {@link Two.Anchor}.
* @description Create a JSON compatible plain object of the current instance. Intended for use with storing values in a database.
* @nota-bene Works in conjunction with {@link Two.Anchor.fromObject}
*/
toObject() {
return {
x: toFixed(this.x),
y: toFixed(this.y),
command: this.command,
relative: this.relative,
controls: {
left: this.controls.left.toObject(),
right: this.controls.right.toObject(),
},
rx: toFixed(this.rx),
ry: toFixed(this.ry),
xAxisRotation: toFixed(this.xAxisRotation),
largeArcFlag: toFixed(this.largeArcFlag),
sweepFlag: toFixed(this.sweepFlag),
};
}
/**
* @name Two.Anchor#toString
* @function
* @returns {String} - A String with comma-separated values reflecting the various values on the current instance.
* @description Create a string form of the current instance. Intended for use with storing values in a database. This is lighter to store than the JSON compatible {@link Two.Anchor#toObject}.
*/
toString() {
return JSON.stringify(this.toObject());
}
}
const proto = {
command: {
enumerable: true,
get: function () {
return this._command;
},
set: function (command) {
if (this._command !== command) {
this._command = command;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
},
},
relative: {
enumerable: true,
get: function () {
return this._relative;
},
set: function (relative) {
if (this._relative !== !!relative) {
this._relative = !!relative;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
},
},
rx: {
enumerable: true,
get: function () {
return this._rx;
},
set: function (rx) {
if (this._rx !== rx) {
this._rx = rx;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
},
},
ry: {
enumerable: true,
get: function () {
return this._ry;
},
set: function (ry) {
if (this._ry !== ry) {
this._ry = ry;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
},
},
xAxisRotation: {
enumerable: true,
get: function () {
return this._xAxisRotation;
},
set: function (xAxisRotation) {
if (this._xAxisRotation !== xAxisRotation) {
this._xAxisRotation = xAxisRotation;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
},
},
largeArcFlag: {
enumerable: true,
get: function () {
return this._largeArcFlag;
},
set: function (largeArcFlag) {
if (this._largeArcFlag !== largeArcFlag) {
this._largeArcFlag = largeArcFlag;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
},
},
sweepFlag: {
get: function () {
return this._sweepFlag;
},
set: function (sweepFlag) {
if (this._sweepFlag !== sweepFlag) {
this._sweepFlag = sweepFlag;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
},
},
};
================================================
FILE: src/children.d.ts
================================================
declare module 'two.js/src/children' {
/**
* @class
* @name Two.Group.Children
* @description A children collection which is accesible both by index and by object `id`.
*/
export class Children extends Collection {
constructor(children?: Shape[]);
constructor(...args: Shape[]);
/**
* @name Two.Group.Children#ids
* @property {Object} - Map of all elements in the list keyed by `id`s.
*/
ids: { [id: string]: Shape };
/**
* @function
* @name Two.Group.Children#attach
* @param {Shape[]} children - The objects which extend {@link Two.Shape} to be added.
* @description Adds elements to the `ids` map.
*/
attach(children: Shape[]): Children;
/**
* @function
* @name Two.Group.Children#detach
* @param {Shape[]} children - The objects which extend {@link Two.Shape} to be removed.
* @description Removes elements to the `ids` map.
*/
detach(children: Shape[]): Children;
}
import { Collection } from 'two.js/src/collection';
import { Shape } from 'two.js/src/shape';
}
================================================
FILE: src/children.js
================================================
import { Events } from './events.js';
import { Collection } from './collection.js';
/**
* @class
* @name Two.Group.Children
* @extends Two.Collection
* @description A children collection which is accesible both by index and by object `id`.
*/
export class Children extends Collection {
/**
* @name Two.Group.Children#ids
* @property {Object} - Map of all elements in the list keyed by `id`s.
*/
// N.B: Technique to disable enumeration on object
#ids = {};
get ids() {
return this.#ids;
}
constructor(children) {
children = Array.isArray(children)
? children
: Array.prototype.slice.call(arguments);
super(children);
this.attach(children);
this.on(Events.Types.insert, this.attach);
this.on(Events.Types.remove, this.detach);
}
/**
* @function
* @name Two.Group.Children#attach
* @param {Two.Shape[]} children - The objects which extend {@link Two.Shape} to be added.
* @description Adds elements to the `ids` map.
*/
attach(children) {
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child && child.id) {
this.ids[child.id] = child;
}
}
return this;
}
/**
* @function
* @name Two.Group.Children#detach
* @param {Two.Shape[]} children - The objects which extend {@link Two.Shape} to be removed.
* @description Removes elements to the `ids` map.
*/
detach(children) {
for (let i = 0; i < children.length; i++) {
delete this.ids[children[i].id];
}
return this;
}
}
================================================
FILE: src/collection.d.ts
================================================
declare module 'two.js/src/collection' {
/**
* @name Two.Collection
* @class
* @description An `Array` like object with additional event propagation on actions. `pop`, `shift`, and `splice` trigger `removed` events. `push`, `unshift`, and `splice` with more than 2 arguments trigger 'inserted'. Finally, `sort` and `reverse` trigger `order` events.
*/
export class Collection extends Array {
constructor(...args: any[]);
/**
* @private
*/
private _events;
private set _bound(arg: boolean);
private get _bound(): boolean;
addEventListener(...args: any[]): any;
on(...args: any[]): any;
bind(...args: any[]): any;
removeEventListener(...args: any[]): any;
off(...args: any[]): any;
unbind(...args: any[]): any;
dispatchEvent(...args: any[]): any;
trigger(...args: any[]): any;
listen(...args: any[]): any;
ignore(...args: any[]): any;
}
}
================================================
FILE: src/collection.js
================================================
import { Events } from './events.js';
/**
* @name Two.Collection
* @class
* @extends Two.Events
* @description An `Array` like object with additional event propagation on actions. `pop`, `shift`, and `splice` trigger `removed` events. `push`, `unshift`, and `splice` with more than 2 arguments trigger 'inserted'. Finally, `sort` and `reverse` trigger `order` events.
*/
export class Collection extends Array {
// Warning: Multiple inheritance hack
/**
* @private
*/
#events = new Events();
// N.B: Technique to disable enumeration on object
get _events() {
return this.#events;
}
set _events(e) {
this.#events = e;
}
// Getters and setters aren't enumerable
get _bound() {
return this.#events._bound;
}
set _bound(v) {
this.#events._bound = v;
}
addEventListener() {
return this.#events.addEventListener?.apply(this, arguments);
}
on() {
return this.#events.on?.apply(this, arguments);
}
bind() {
return this.#events.bind?.apply(this, arguments);
}
removeEventListener() {
return this.#events.removeEventListener?.apply(this, arguments);
}
off() {
return this.#events.off?.apply(this, arguments);
}
unbind() {
return this.#events.unbind?.apply(this, arguments);
}
dispatchEvent() {
return this.#events.dispatchEvent?.apply(this, arguments);
}
trigger() {
return this.#events.trigger?.apply(this, arguments);
}
listen() {
return this.#events.listen?.apply(this, arguments);
}
ignore() {
return this.#events.ignore?.apply(this, arguments);
}
constructor() {
super();
if (arguments[0] && Array.isArray(arguments[0])) {
if (arguments[0].length > 0) {
this.push.apply(this, arguments[0]);
}
} else if (arguments.length > 0) {
this.push.apply(this, arguments);
}
}
pop() {
const popped = super.pop.apply(this, arguments);
this.trigger(Events.Types.remove, [popped]);
return popped;
}
shift() {
const shifted = super.shift.apply(this, arguments);
this.trigger(Events.Types.remove, [shifted]);
return shifted;
}
push() {
const pushed = super.push.apply(this, arguments);
this.trigger(Events.Types.insert, arguments);
return pushed;
}
unshift() {
const unshifted = super.unshift.apply(this, arguments);
this.trigger(Events.Types.insert, arguments);
return unshifted;
}
splice() {
const spliced = super.splice.apply(this, arguments);
this.trigger(Events.Types.remove, spliced);
if (arguments.length > 2) {
const inserted = this.slice(
arguments[0],
arguments[0] + arguments.length - 2
);
this.trigger(Events.Types.insert, inserted);
this.trigger(Events.Types.order);
}
return spliced;
}
sort() {
super.sort.apply(this, arguments);
this.trigger(Events.Types.order);
return this;
}
reverse() {
super.reverse.apply(this, arguments);
this.trigger(Events.Types.order);
return this;
}
indexOf() {
return super.indexOf.apply(this, arguments);
}
map(func, scope) {
const results = [];
for (let key = 0; key < this.length; key++) {
const value = this[key];
let result;
if (scope) {
result = func.call(scope, value, key);
} else {
result = func(value, key);
}
results.push(result);
}
return results;
}
}
================================================
FILE: src/constants.d.ts
================================================
declare module 'two.js/src/constants' {
export interface Constants {
NextFrameId: number;
Types: {
webgl: 'WebGLRenderer';
svg: 'SVGRenderer';
canvas: 'CanvasRenderer';
};
Version: string;
PublishDate: string;
Identifier: string;
Resolution: number;
AutoCalculateImportedMatrices: boolean;
Instances: Two[];
uniqueId(): number;
}
import Two from 'two.js';
}
================================================
FILE: src/constants.js
================================================
let count = 0;
export const Constants = {
/**
* @name Two.NextFrameId
* @property {Number}
* @description The id of the next `requestAnimationFrame` function. Used to control the (or cancel) the default behavior of Two.js animation loops.
*/
NextFrameId: null,
// Primitive
/**
* @name Two.Types
* @property {Object} - The different rendering types available in the library.
*/
Types: {
webgl: 'WebGLRenderer',
svg: 'SVGRenderer',
canvas: 'CanvasRenderer',
},
/**
* @name Two.Version
* @property {String} - The current working version of the library.
*/
Version: '<%= version %>',
/**
* @name Two.PublishDate
* @property {String} - The automatically generated publish date in the build process to verify version release candidates.
*/
PublishDate: '<%= publishDate %>',
/**
* @name Two.Identifier
* @property {String} - String prefix for all Two.js object's ids. This trickles down to SVG ids.
*/
Identifier: 'two-',
/**
* @name Two.Resolution
* @property {Number} - Default amount of vertices to be used for interpreting Arcs and ArcSegments.
*/
Resolution: 12,
/**
* @name Two.AutoCalculateImportedMatrices
* @property {Boolean} - When importing SVGs through the {@link Two#interpret} and {@link Two#load}, this boolean determines whether Two.js infers and then overrides the exact transformation matrix of the reference SVG.
* @nota-bene `false` copies the exact transformation matrix values, but also sets the path's `matrix.manual = true`.
*/
AutoCalculateImportedMatrices: true,
/**
* @name Two.Instances
* @property {Two[]} - Registered list of all Two.js instances in the current session.
*/
Instances: [],
/**
* @function Two.uniqueId
* @description Simple method to access an incrementing value. Used for `id` allocation on all Two.js objects.
* @returns {Number} Ever increasing Number.
*/
uniqueId: function () {
return count++;
},
};
================================================
FILE: src/effects/gradient.d.ts
================================================
declare module 'two.js/src/effects/gradient' {
type SpreadProperties = 'pad' | 'reflect' | 'repeat';
type UnitsProperties = 'userSpaceOnUse' | 'objectBoundingBox';
/**
* @name Two.Gradient
* @class
* @param {Stop[]} [stops] - A list of {@link Two.Stop}s that contain the gradient fill pattern for the gradient.
* @description This is the base class for constructing different types of gradients with Two.js. The two common gradients are {@link Two.LinearGradient} and {@link Two.RadialGradient}.
*/
export class Gradient extends TwoElement {
/**
* @name Two.Gradient.Stop
* @see {@link Two.Stop}
*/
static Stop: Stop;
/**
* @name Two.Gradient.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Gradient}.
*/
static Properties: ('spread' | 'stops' | 'units' | string)[];
/**
* @name Two.Gradient.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Gradient} to create a new instance
* @returns {Two.Gradient}
* @description Create a new {@link Two.Gradient} from an object notation of a {@link Two.Gradient}.
* @nota-bene Works in conjunction with {@link Two.Gradient#toObject}
*/
static fromObject(
obj: Parameters[0] & {
stops?: number[];
spread?: SpreadProperties;
units?: UnitsProperties;
}
): Gradient;
constructor(stops?: Stop[]);
private _flagStops: boolean;
private _flagSpread: boolean;
private _flagUnits: boolean;
private _spread: string;
private _units: string;
/**
* @name Two.Gradient#renderer
* @property {Object}
* @description Object access to store relevant renderer specific variables. Warning: manipulating this object can create unintended consequences.
* @nota-bene With the {@link Two.SVGRenderer} you can access the underlying SVG element created via `shape.renderer.elem`.
*/
/**
* @name Two.Gradient#id
* @property {String} - Session specific unique identifier.
* @nota-bene In the {@link Two.SVGRenderer} change this to change the underlying SVG element's id too.
*/
id: string;
/**
* @name Two.Gradient#spread
* @property {String} - Indicates what happens if the gradient starts or ends inside the bounds of the target rectangle. Possible values are `'pad'`, `'reflect'`, and `'repeat'`.
* @see {@link https://www.w3.org/TR/SVG11/pservers.html#LinearGradientElementSpreadMethodAttribute} for more information
*/
spread: SpreadProperties;
/**
* @name Two.Gradient#units
* @property {String} [units='objectBoundingBox'] - Indicates how coordinate values are interpreted by the renderer. Possible values are `'userSpaceOnUse'` and `'objectBoundingBox'`.
* @see {@link https://www.w3.org/TR/SVG11/pservers.html#RadialGradientElementGradientUnitsAttribute} for more information
*/
units: UnitsProperties;
/**
* @name Two.Gradient#stops
* @property {Two.Stop[]} - An ordered list of {@link Two.Stop}s for rendering the gradient.
* @nota-bene Actually a {@link Two.Collection} polyfilled to act like an observable Array.
*/
stops: Stop[];
/**
* @name Two.Gradient#copy
* @function
* @param {Two.Gradient} gradient - The reference {@link Two.Gradient}
* @description Copy the properties of one {@link Two.Gradient} onto another.
*/
copy(gradient: Gradient): Gradient;
/**
* @name Two.Gradient#clone
* @function
* @param {Group} [parent] - The parent group or scene to add the clone to.
* @returns {Gradient}
* @description Create a new instance of {@link Two.Gradient} with the same properties of the current path.
*/
clone(parent?: Group): Gradient;
/**
* @name Two.Gradient#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject(): object;
/**
* @name Two.Gradient#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
protected _update(bubbles?: boolean): Gradient;
/**
* @name Two.Gradient#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset(): Gradient;
/**
* @name Two.Gradient#dispose
* @function
* @description Detach instance from renderer including any `` or textures stored in memory.
*/
dispose(): Gradient;
}
import { Element as TwoElement } from 'two.js/src/element';
import { Stop } from 'two.js/src/effects/stop';
import { Group } from 'two.js/src/group';
}
================================================
FILE: src/effects/gradient.js
================================================
import { Collection } from '../collection.js';
import { Events } from '../events.js';
import { Element } from '../element.js';
import { _ } from '../utils/underscore.js';
import { Stop } from './stop.js';
/**
* @name Two.Gradient
* @class
* @extends Two.Element
* @param {Two.Stop[]} [stops] - A list of {@link Two.Stop}s that contain the gradient fill pattern for the gradient.
* @description This is the base class for constructing different types of gradients with Two.js. The two common gradients are {@link Two.LinearGradient} and {@link Two.RadialGradient}.
*/
export class Gradient extends Element {
_flagStops = false;
_flagSpread = false;
_flagUnits = false;
_spread = '';
_units = '';
constructor(stops) {
super();
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this._renderer.type = 'gradient';
this._renderer.flagStops = FlagStops.bind(this);
this._renderer.bindStops = BindStops.bind(this);
this._renderer.unbindStops = UnbindStops.bind(this);
/**
* @name Two.Gradient#spread
* @property {String} - Indicates what happens if the gradient starts or ends inside the bounds of the target rectangle. Possible values are `'pad'`, `'reflect'`, and `'repeat'`.
* @see {@link https://www.w3.org/TR/SVG11/pservers.html#LinearGradientElementSpreadMethodAttribute} for more information
*/
this.spread = 'pad';
/**
* @name Two.Gradient#units
* @property {String} [units='objectBoundingBox'] - Indicates how coordinate values are interpreted by the renderer. Possible values are `'userSpaceOnUse'` and `'objectBoundingBox'`.
* @see {@link https://www.w3.org/TR/SVG11/pservers.html#RadialGradientElementGradientUnitsAttribute} for more information
*/
this.units = 'objectBoundingBox';
/**
* @name Two.Gradient#stops
* @property {Two.Stop[]} - An ordered list of {@link Two.Stop}s for rendering the gradient.
*/
if (stops) {
this.stops = stops;
}
}
/**
* @name Two.Gradient.Stop
* @see {@link Two.Stop}
*/
static Stop = Stop;
/**
* @name Two.Gradient.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Gradient}.
*/
static Properties = ['spread', 'stops', 'units'];
/**
* @name Two.Gradient.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Gradient} to create a new instance
* @returns {Two.Gradient}
* @description Create a new {@link Two.Gradient} from an object notation of a {@link Two.Gradient}.
* @nota-bene Works in conjunction with {@link Two.Gradient#toObject}
*/
static fromObject(obj) {
let stops = obj.stops;
if (stops && stops.length > 0) {
stops = stops.map((o) => (o instanceof Stop ? o : new Stop().copy(o)));
}
const gradient = new Gradient(stops).copy(obj);
if ('id' in obj) {
gradient.id = obj.id;
}
return gradient;
}
/**
* @name Two.Gradient#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Gradient}
* @description Create a new instance of {@link Two.Gradient} with the same properties of the current path.
*/
clone(parent) {
const stops = this.stops.map((s) => {
return s.clone();
});
const clone = new Gradient(stops);
_.each(
Gradient.Properties,
(k) => {
clone[k] = this[k];
},
this
);
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Gradient#copy
* @function
* @param {Two.Gradient} gradient - The reference {@link Two.Gradient}
* @description Copy the properties of one {@link Two.Gradient} onto another.
*/
copy(gradient) {
super.copy.call(this, gradient);
for (let i = 0; i < Gradient.Properties.length; i++) {
const k = Gradient.Properties[i];
if (k in gradient) {
this[k] = gradient[k];
}
}
return this;
}
/**
* @name Two.Gradient#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const result = {
stops: this.stops.map((s) => {
return s.toObject();
}),
};
_.each(
Gradient.Properties,
(k) => {
result[k] = this[k];
},
this
);
return result;
}
/**
* @name Two.Gradient#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagSpread || this._flagStops) {
this.trigger(Events.Types.change);
}
return this;
}
/**
* @name Two.Gradient#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagSpread = this._flagUnits = this._flagStops = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Gradient#dispose
* @function
* @description Detach instance from renderer including any `` or textures stored in memory.
*/
dispose() {
// Remove gradient from SVG document
if ('elem' in this._renderer) {
const elem = this._renderer.elem;
elem.parentNode.removeChild(elem);
}
// Deallocate textures from the graphics card
if ('effect' in this._renderer) {
this._renderer.effect = null;
}
return this;
}
}
const proto = {
spread: {
enumerable: true,
get: function () {
return this._spread;
},
set: function (v) {
this._spread = v;
this._flagSpread = true;
},
},
units: {
enumerable: true,
get: function () {
return this._units;
},
set: function (v) {
this._units = v;
this._flagUnits = true;
},
},
stops: {
enumerable: true,
get: function () {
return this._stops;
},
set: function (stops) {
const bindStops = this._renderer.bindStops;
const unbindStops = this._renderer.unbindStops;
// Remove previous listeners
if (this._stops) {
this._stops
.unbind(Events.Types.insert, bindStops)
.unbind(Events.Types.remove, unbindStops);
}
// Create new Collection with copy of Stops
this._stops = new Collection((stops || []).slice(0));
// Listen for Collection changes and bind / unbind
this._stops
.bind(Events.Types.insert, bindStops)
.bind(Events.Types.remove, unbindStops);
// Bind Initial Stops
bindStops(this._stops);
},
},
};
/**
* @name FlagStops
* @private
* @function
* @description Cached method to let renderers know stops have been updated on a {@link Two.Gradient}.
*/
function FlagStops() {
this._flagStops = true;
}
/**
* @name BindVertices
* @private
* @function
* @description Cached method to let {@link Two.Gradient} know vertices have been added to the instance.
*/
function BindStops(items) {
// This function is called a lot
// when importing a large SVG
let i = items.length;
while (i--) {
items[i].bind(Events.Types.change, this._renderer.flagStops);
items[i].parent = this;
}
this._renderer.flagStops();
}
/**
* @name UnbindStops
* @private
* @function
* @description Cached method to let {@link Two.Gradient} know vertices have been removed from the instance.
*/
function UnbindStops(items) {
let i = items.length;
while (i--) {
items[i].unbind(Events.Types.change, this._renderer.flagStops);
delete items[i].parent;
}
this._renderer.flagStops();
}
================================================
FILE: src/effects/image-sequence.d.ts
================================================
declare module 'two.js/src/effects/image-sequence' {
/**
* @name Two.ImageSequence
* @class
* @param {String|String[]|Texture|Texture[]} [src] - A list of URLs or {@link Two.Texture}s.
* @param {Number} [ox=0] - The initial `x` position of the Two.ImageSequence.
* @param {Number} [oy=0] - The initial `y` position of the Two.ImageSequence.
* @param {Number} [frameRate=30] - The frame rate at which the images should playback at.
* @description A convenient package to display still or animated images organized as a series of still images.
*/
export class ImageSequence extends Rectangle {
/**
* @name Two.ImageSequence.Properties
* @property {String[]} - A list of properties that are on every {@link Two.ImageSequence}.
*/
Properties: (
| 'textures'
| 'frameRate'
| 'index'
| 'firstFrame'
| 'lastFrame'
| 'loop'
| string
)[];
/**
* @name Two.ImageSequence.DefaultFrameRate
* @property The default frame rate that {@link Two.ImageSequence#frameRate} is set to when instantiated.
*/
static DefaultFrameRate: 30;
/**
* @name Two.ImageSequence.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.ImageSequence} to create a new instance
* @returns {Two.ImageSequence}
* @description Create a new {@link Two.ImageSequence} from an object notation of a {@link Two.ImageSequence}.
* @nota-bene Works in conjunction with {@link Two.ImageSequence#toObject}
*/
fromObject(
obj: Parameters[0] & {
textures?: Parameters[0][];
frameRate?: number;
index?: number;
firstFrame?: number;
lastFrame?: number;
loop?: boolean;
}
): ImageSequence;
constructor(
src?: string | string[] | Texture | Texture[],
ox?: number,
oy?: number,
frameRate?: number
);
/**
* @name Two.ImageSequence#_flagTextures
* @private
* @property {Boolean} - Determines whether the {@link Two.ImageSequence#textures} need updating.
*/
private _flagTextures;
/**
* @name Two.ImageSequence#_flagFrameRate
* @private
* @property {Boolean} - Determines whether the {@link Two.ImageSequence#frameRate} needs updating.
*/
private _flagFrameRate;
/**
* @name Two.ImageSequence#_flagIndex
* @private
* @property {Boolean} - Determines whether the {@link Two.ImageSequence#index} needs updating.
*/
private _flagIndex;
/**
* @name Two.ImageSequence#_amount
* @private
* @property {Number} - Number of frames for a given {@link Two.ImageSequence}.
*/
private _amount;
/**
* @name Two.ImageSequence#_duration
* @private
* @property {Number} - Number of milliseconds a {@link Two.ImageSequence}.
*/
private _duration;
/**
* @name Two.ImageSequence#_index
* @private
* @property {Number} - The current frame the {@link Two.ImageSequence} is currently displaying.
*/
private _index;
/**
* @name Two.ImageSequence#_startTime
* @private
* @property {Milliseconds} - Epoch time in milliseconds of when the {@link Two.ImageSequence} started.
*/
private _startTime;
/**
* @name Two.ImageSequence#_playing
* @private
* @property {Boolean} - Dictates whether the {@link Two.ImageSequence} is animating or not.
*/
private _playing;
/**
* @name Two.ImageSequence#_firstFrame
* @private
* @property {Number} - The frame the {@link Two.ImageSequence} should start with.
*/
private _firstFrame;
/**
* @name Two.ImageSequence#_lastFrame
* @private
* @property {Number} - The frame the {@link Two.ImageSequence} should end with.
*/
private _lastFrame;
/**
* @name Two.ImageSequence#_playing
* @private
* @property {Boolean} - Dictates whether the {@link Two.ImageSequence} should loop or not.
*/
private _loop;
/**
* @name Two.ImageSequence#_textures
* @private
* @see {@link Two.ImageSequence#textures}
*/
private _textures;
/**
* @name Two.ImageSequence#_frameRate
* @private
* @see {@link Two.ImageSequence#frameRate}
*/
private _frameRate;
textures: Texture[];
frameRate: number;
/**
* @name Two.ImageSequence#index
* @property {Number} - The index of the current tile of the sprite to display. Defaults to `0`.
*/
index: number;
/**
* @name Two.ImageSequence#copy
* @function
* @param {Two.ImageSequence} imageSequence - The reference {@link Two.ImageSequence}
* @description Copy the properties of one {@link Two.ImageSequence} onto another.
*/
copy(imageSeqence: ImageSequence): ImageSequence;
/**
* @name Two.ImageSequence#play
* @function
* @param {Number} [firstFrame=0] - The index of the frame to start the animation with.
* @param {Number} [lastFrame] - The index of the frame to end the animation with. Defaults to the last item in the {@link Two.ImageSequence#textures}.
* @param {Function} [onLastFrame] - Optional callback function to be triggered after playing the last frame. This fires multiple times when the image sequence is looped.
* @description Initiate animation playback of a {@link Two.ImageSequence}.
*/
play(
firstFrame?: number,
lastFrame?: number,
onLastFrame?: () => void
): ImageSequence;
/**
* @name Two.ImageSequence#pause
* @function
* @description Halt animation playback of a {@link Two.ImageSequence}.
*/
pause(): ImageSequence;
/**
* @name Two.ImageSequence#stop
* @function
* @description Halt animation playback of a {@link Two.ImageSequence} and set the current frame back to the first frame.
*/
stop(): ImageSequence;
/**
* @name Two.ImageSequence#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.ImageSequence}
* @description Create a new instance of {@link Two.ImageSequence} with the same properties of the current image sequence.
*/
clone(parent: Group): ImageSequence;
/**
* @name Two.ImageSequence#dispose
* @function
* @description Release the image sequence's renderer resources and detach all events.
* This method stops any running animation, clears animation callbacks, unbinds
* textures collection events, and disposes individual textures (calling dispose()
* for thorough cleanup) while preserving the renderer type for potential
* re-attachment to a new renderer.
*/
dispose(): ImageSequence;
}
import { Rectangle } from 'two.js/src/shapes/rectangle';
import { Texture } from 'two.js/src/effects/texture';
import { Group } from 'two.js/src/group';
}
================================================
FILE: src/effects/image-sequence.js
================================================
import { Collection } from '../collection.js';
import { Events } from '../events.js';
import { lerp } from '../utils/math.js';
import { _ } from '../utils/underscore.js';
import { Vector } from '../vector.js';
import { Rectangle } from '../shapes/rectangle.js';
import { Texture } from './texture.js';
/**
* @name Two.ImageSequence
* @class
* @extends Two.Rectangle
* @param {String|String[]|Two.Texture|Two.Texture[]} [src] - A list of URLs or {@link Two.Texture}s.
* @param {Number} [ox=0] - The initial `x` position of the Two.ImageSequence.
* @param {Number} [oy=0] - The initial `y` position of the Two.ImageSequence.
* @param {Number} [frameRate=30] - The frame rate at which the images should playback at.
* @description A convenient package to display still or animated images organized as a series of still images.
*/
export class ImageSequence extends Rectangle {
/**
* @name Two.ImageSequence#_flagTextures
* @private
* @property {Boolean} - Determines whether the {@link Two.ImageSequence#textures} need updating.
*/
_flagTextures = false;
/**
* @name Two.ImageSequence#_flagFrameRate
* @private
* @property {Boolean} - Determines whether the {@link Two.ImageSequence#frameRate} needs updating.
*/
_flagFrameRate = false;
/**
* @name Two.ImageSequence#_flagIndex
* @private
* @property {Boolean} - Determines whether the {@link Two.ImageSequence#index} needs updating.
*/
_flagIndex = false;
// Private variables
/**
* @name Two.ImageSequence#_amount
* @private
* @property {Number} - Number of frames for a given {@link Two.ImageSequence}.
*/
_amount = 1;
/**
* @name Two.ImageSequence#_duration
* @private
* @property {Number} - Number of milliseconds a {@link Two.ImageSequence}.
*/
_duration = 0;
/**
* @name Two.ImageSequence#_index
* @private
* @property {Number} - The current frame the {@link Two.ImageSequence} is currently displaying.
*/
_index = 0;
/**
* @name Two.ImageSequence#_startTime
* @private
* @property {Milliseconds} - Epoch time in milliseconds of when the {@link Two.ImageSequence} started.
*/
_startTime = 0;
/**
* @name Two.ImageSequence#_playing
* @private
* @property {Boolean} - Dictates whether the {@link Two.ImageSequence} is animating or not.
*/
_playing = false;
/**
* @name Two.ImageSequence#_firstFrame
* @private
* @property {Number} - The frame the {@link Two.ImageSequence} should start with.
*/
_firstFrame = 0;
/**
* @name Two.ImageSequence#_lastFrame
* @private
* @property {Number} - The frame the {@link Two.ImageSequence} should end with.
*/
_lastFrame = 0;
/**
* @name Two.ImageSequence#_playing
* @private
* @property {Boolean} - Dictates whether the {@link Two.ImageSequence} should loop or not.
*/
_loop = true;
// Exposed through getter-setter
/**
* @name Two.ImageSequence#_textures
* @private
* @see {@link Two.ImageSequence#textures}
*/
_textures = null;
/**
* @name Two.ImageSequence#_frameRate
* @private
* @see {@link Two.ImageSequence#frameRate}
*/
_frameRate = 0;
/**
* @name Two.ImageSequence#_origin
* @private
* @see {@link Two.ImageSequence#origin}
*/
_origin = null;
constructor(src, ox, oy, frameRate) {
super(ox, oy, 0, 0);
this._renderer.type = 'image-sequence';
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this._renderer.flagTextures = FlagTextures.bind(this);
this._renderer.bindTextures = BindTextures.bind(this);
this._renderer.unbindTextures = UnbindTextures.bind(this);
this.noStroke();
this.noFill();
/**
* @name Two.ImageSequence#textures
* @property {Two.Texture[]} - A list of textures to be used as frames for animating the {@link Two.ImageSequence}.
*/
if (Array.isArray(src)) {
this.textures = src.map(GenerateTexture.bind(this));
} else if (typeof src === 'string') {
// If just a single src convert into a single Two.Texture
this.textures = [GenerateTexture(src)];
}
this.origin = new Vector();
this._update();
/**
* @name Two.ImageSequence#frameRate
* @property {Number} - The number of frames to animate against per second.
*/
if (typeof frameRate === 'number') {
this.frameRate = frameRate;
} else {
this.frameRate = ImageSequence.DefaultFrameRate;
}
/**
* @name Two.ImageSequence#index
* @property {Number} - The index of the current tile of the sprite to display. Defaults to `0`.
*/
this.index = 0;
}
/**
* @name Two.ImageSequence.Properties
* @property {String[]} - A list of properties that are on every {@link Two.ImageSequence}.
*/
static Properties = [
'textures',
'frameRate',
'index',
'firstFrame',
'lastFrame',
'loop',
];
/**
* @name Two.ImageSequence.DefaultFrameRate
* @property The default frame rate that {@link Two.ImageSequence#frameRate} is set to when instantiated.
*/
static DefaultFrameRate = 30;
/**
* @name Two.ImageSequence.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.ImageSequence} to create a new instance
* @returns {Two.ImageSequence}
* @description Create a new {@link Two.ImageSequence} from an object notation of a {@link Two.ImageSequence}.
* @nota-bene Works in conjunction with {@link Two.ImageSequence#toObject}
*/
static fromObject(obj) {
const sequence = new ImageSequence().copy(obj);
if ('id' in obj) {
sequence.id = obj.id;
}
return sequence;
}
/**
* @name Two.ImageSequence#copy
* @function
* @param {Two.ImageSequence} imageSequence - The reference {@link Two.ImageSequence}
* @description Copy the properties of one {@link Two.ImageSequence} onto another.
*/
copy(imageSequence) {
super.copy.call(this, imageSequence);
for (let i = 0; i < ImageSequence.Properties.length; i++) {
const k = ImageSequence.Properties[i];
if (k in imageSequence) {
this[k] = imageSequence[k];
}
}
return this;
}
/**
* @name Two.ImageSequence#play
* @function
* @param {Number} [firstFrame=0] - The index of the frame to start the animation with.
* @param {Number} [lastFrame] - The index of the frame to end the animation with. Defaults to the last item in the {@link Two.ImageSequence#textures}.
* @param {Function} [onLastFrame] - Optional callback function to be triggered after playing the last frame. This fires multiple times when the image sequence is looped.
* @description Initiate animation playback of a {@link Two.ImageSequence}.
*/
play(firstFrame, lastFrame, onLastFrame) {
this._playing = true;
this._firstFrame = 0;
this._lastFrame = this.amount - 1;
this._startTime = _.performance.now();
if (typeof firstFrame === 'number') {
this._firstFrame = firstFrame;
}
if (typeof lastFrame === 'number') {
this._lastFrame = lastFrame;
}
if (typeof onLastFrame === 'function') {
this._onLastFrame = onLastFrame;
} else {
delete this._onLastFrame;
}
if (this._index !== this._firstFrame) {
this._startTime -=
(1000 * Math.abs(this._index - this._firstFrame)) / this._frameRate;
}
return this;
}
/**
* @name Two.ImageSequence#pause
* @function
* @description Halt animation playback of a {@link Two.ImageSequence}.
*/
pause() {
this._playing = false;
return this;
}
/**
* @name Two.ImageSequence#stop
* @function
* @description Halt animation playback of a {@link Two.ImageSequence} and set the current frame back to the first frame.
*/
stop() {
this._playing = false;
this._index = this._firstFrame;
return this;
}
/**
* @name Two.ImageSequence#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.ImageSequence}
* @description Create a new instance of {@link Two.ImageSequence} with the same properties of the current image sequence.
*/
clone(parent) {
const clone = new ImageSequence(
this.textures,
this.translation.x,
this.translation.y,
this.frameRate
);
clone._loop = this._loop;
if (this._playing) {
clone.play();
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.ImageSequence#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = 'image-sequence';
object.textures = this.textures.map(function (texture) {
return texture.toObject();
});
object.frameRate = this.frameRate;
object.index = this.index;
object.firstFrame = this.firstFrame;
object.lastFrame = this.lastFrame;
object.loop = this.loop;
return object;
}
/**
* @name Two.ImageSequence#dispose
* @function
* @returns {Two.ImageSequence}
* @description Release the image sequence's renderer resources and detach all events.
* This method stops any running animation, clears animation callbacks, unbinds
* textures collection events, and disposes individual textures (calling dispose()
* for thorough cleanup) while preserving the renderer type for potential
* re-attachment to a new renderer.
*/
dispose() {
// Call parent dispose to preserve renderer type and unbind events
super.dispose();
// Stop animation if playing
if (this._playing) {
this._playing = false;
}
// Clear animation callbacks
this._onLastFrame = null;
// Unbind textures collection events
if (this.textures && typeof this.textures.unbind === 'function') {
try {
this.textures.unbind();
} catch (e) {
// Ignore unbind errors for incomplete Collection objects
}
}
// Dispose individual textures (more thorough than unbind)
if (this.textures) {
for (let i = 0; i < this.textures.length; i++) {
const texture = this.textures[i];
if (typeof texture.dispose === 'function') {
texture.dispose();
} else if (typeof texture.unbind === 'function') {
texture.unbind();
}
}
}
return this;
}
/**
* @name Two.ImageSequence#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
const effect = this._textures;
let width, height, elapsed, amount, duration, texture;
let index, frames;
if (effect) {
if (this._flagTextures) {
this._amount = effect.length;
}
if (this._flagFrameRate) {
this._duration = (1000 * this._amount) / this._frameRate;
}
if (this._playing && this._frameRate > 0) {
amount = this._amount;
if (_.isNaN(this._lastFrame)) {
this._lastFrame = amount - 1;
}
// TODO: Offload perf logic to instance of `Two`.
elapsed = _.performance.now() - this._startTime;
frames = this._lastFrame + 1;
duration = (1000 * (frames - this._firstFrame)) / this._frameRate;
if (this._loop) {
elapsed = elapsed % duration;
} else {
elapsed = Math.min(elapsed, duration);
}
index = lerp(this._firstFrame, frames, elapsed / duration);
index = Math.floor(index);
if (index !== this._index) {
this._index = index;
texture = effect[this._index];
if (texture.loaded) {
width = texture.image.width;
height = texture.image.height;
if (this.width !== width) {
this.width = width;
}
if (this.height !== height) {
this.height = height;
}
this.fill = texture;
if (index >= this._lastFrame - 1 && this._onLastFrame) {
this._onLastFrame(); // Shortcut for chainable sprite animations
}
}
}
} else if (this._flagIndex || !(this.fill instanceof Texture)) {
texture = effect[this._index];
if (texture.loaded) {
width = texture.image.width;
height = texture.image.height;
if (this.width !== width) {
this.width = width;
}
if (this.height !== height) {
this.height = height;
}
}
this.fill = texture;
}
}
super._update.call(this);
return this;
}
/**
* @name Two.ImageSequence#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagTextures = this._flagFrameRate = false;
super.flagReset.call(this);
return this;
}
}
const proto = {
frameRate: {
enumerable: true,
get: function () {
return this._frameRate;
},
set: function (v) {
this._frameRate = v;
this._flagFrameRate = true;
},
},
index: {
enumerable: true,
get: function () {
return this._index;
},
set: function (v) {
this._index = v;
this._flagIndex = true;
},
},
textures: {
enumerable: true,
get: function () {
return this._textures;
},
set: function (textures) {
const bindTextures = this._renderer.bindTextures;
const unbindTextures = this._renderer.unbindTextures;
// Remove previous listeners
if (this._textures) {
this._textures
.unbind(Events.Types.insert, bindTextures)
.unbind(Events.Types.remove, unbindTextures);
}
// Create new Collection with copy of vertices
this._textures = new Collection((textures || []).slice(0));
// Listen for Collection changes and bind / unbind
this._textures
.bind(Events.Types.insert, bindTextures)
.bind(Events.Types.remove, unbindTextures);
// Bind Initial Textures
bindTextures(this._textures);
},
},
firstFrame: {
enumerable: true,
get: function () {
return this._firstFrame;
},
set: function (v) {
this._firstFrame = v;
},
},
lastFrame: {
enumerable: true,
get: function () {
return this._lastFrame;
},
set: function (v) {
this._lastFrame = v;
},
},
loop: {
enumerable: true,
get: function () {
return this._loop;
},
set: function (v) {
this._loop = !!v;
},
},
};
/**
* @name FlagTextures
* @private
* @function
* @description Cached method to let renderers know textures have been updated on a {@link Two.ImageSequence}.
*/
function FlagTextures() {
this._flagTextures = true;
}
/**
* @name BindTextures
* @private
* @function
* @description Cached method to let {@link Two.ImageSequence} know textures have been added to the instance.
*/
function BindTextures(items) {
let i = items.length;
while (i--) {
items[i].bind(Events.Types.change, this._renderer.flagTextures);
}
this._renderer.flagTextures();
}
/**
* @name UnbindTextures
* @private
* @function
* @description Cached method to let {@link Two.ImageSequence} know textures have been removed from the instance.
*/
function UnbindTextures(items) {
let i = items.length;
while (i--) {
items[i].unbind(Events.Types.change, this._renderer.flagTextures);
}
this._renderer.flagTextures();
}
/**
* @name GenerateTexture
* @private
* @property {Function} - Shorthand function to prepare source image material into readable format by {@link Two.ImageSequence}.
* @param {String|Two.Texture} textureOrString - The texture or string to create a {@link Two.Texture} from.
* @description Function used internally by {@link Two.ImageSequence} to parse arguments and return {@link Two.Texture}s.
* @returns {Two.Texture}
*/
function GenerateTexture(obj) {
if (obj instanceof Texture) {
return obj;
} else if (typeof obj === 'string') {
return new Texture(obj);
}
}
================================================
FILE: src/effects/image.d.ts
================================================
declare module 'two.js/src/effects/image' {
export type ModeProperties = 'fill' | 'fit' | 'crop' | 'tile' | 'stretch';
/**
* @name Two.Image
* @class
* @extends Two.Rectangle
* @param {String|Two.Texture} [src] - The URL path or {@link Two.Texture} to be used as the bitmap data displayed on the image.
* @param {Number} [ox=0] - The initial `x` position of the Two.Image.
* @param {Number} [oy=0] - The initial `y` position of the Two.Image.
* @param {Number} [width=1] - The width to display the image at.
* @param {Number} [height=1] - The height to display the image at.
* @description A convenient package to display images scaled to fit specific dimensions. Unlike {@link Two.Sprite}, this class scales the image to the provided width and height rather than using the image's native dimensions. By default, images are scaled to 'fill' within the bounds while preserving aspect ratio.
*/
export class Image extends Rectangle {
/**
* @name Two.Image.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Image}.
*/
static Properties: ('texture' | 'mode' | string)[];
/**
* @name Two.Image.Modes
* @property {Object} mode - Different mode types to render an image inspired by Figma.
* @property {String} mode.fill - Scale image to fill the bounds while preserving aspect ratio.
* @property {String} mode.fit - Scale image to fit within bounds while preserving aspect ratio.
* @property {String} mode.crop - Scale image to fill bounds while preserving aspect ratio, cropping excess.
* @property {String} mode.tile - Repeat image at original size to fill the bounds.
* @property {String} mode.stretch - Stretch image to fill dimensions, ignoring aspect ratio.
*/
static Modes: {
fill: 'fill';
fit: 'fit';
crop: 'crop';
tile: 'tile';
stretch: 'stretch';
};
/**
* @name Two.Image.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Image} to create a new instance
* @returns {Two.Image}
* @description Create a new {@link Two.Image} from an object notation of a {@link Two.Image}.
* @nota-bene Works in conjunction with {@link Two.Image#toObject}
*/
static fromObject(
obj: Parameters[0] & {
texture?: Parameters[0];
mode?: ModeProperties;
}
): Image;
constructor(
src?: string | Texture,
ox?: number,
oy?: number,
width?: number,
height?: number,
mode?: ModeProperties
);
/**
* @name Two.Image#_flagTexture
* @private
* @property {Boolean} - Determines whether the {@link Two.Image#texture} needs updating.
*/
private _flagTexture;
/**
* @name Two.Image#_flagMode
* @private
* @property {Boolean} - Determines whether the {@link Two.Image#mode} needs updating.
*/
private _flagMode;
/**
* @name Two.Image#_texture
* @private
* @see {@link Two.Image#texture}
*/
private _texture;
/**
* @name Two.Image#_mode
* @private
* @see {@link Two.Image#mode}
*/
private _mode;
texture: Texture;
/**
* @name Two.Image#mode
* @property {String} - The scaling mode for the image. Can be 'fill', 'fit', 'crop', 'tile', or 'stretch'. Defaults to 'fill'.
*/
mode: ModeProperties;
/**
* @name Two.Image#dispose
* @function
* @description Release the image's renderer resources and detach all events.
* This method disposes the texture (calling dispose() for thorough cleanup) and inherits comprehensive
* cleanup from the Rectangle/Path hierarchy while preserving the renderer type
* for potential re-attachment.
*/
dispose(): Image;
}
import { Rectangle } from 'two.js/src/shapes/rectangle';
import { Texture } from 'two.js/src/effects/texture';
}
================================================
FILE: src/effects/image.js
================================================
import { Vector } from '../vector.js';
import { Rectangle } from '../shapes/rectangle.js';
import { Texture } from './texture.js';
/**
* @name Two.Image
* @class
* @extends Two.Rectangle
* @param {String|Two.Texture} [src] - The URL path or {@link Two.Texture} to be used as the bitmap data displayed on the image.
* @param {Number} [ox=0] - The initial `x` position of the Two.Image.
* @param {Number} [oy=0] - The initial `y` position of the Two.Image.
* @param {Number} [width=1] - The width to display the image at.
* @param {Number} [height=1] - The height to display the image at.
* @param {String} [mode="fill"] - The fill mode
* @description A convenient package to display images scaled to fit specific dimensions. Unlike {@link Two.Sprite}, this class scales the image to the provided width and height rather than using the image's native dimensions. By default, images are scaled to 'fill' within the bounds while preserving aspect ratio.
* @nota-bene Two.Image.fit mode in all renderers is not complete
*/
export class Image extends Rectangle {
/**
* @name Two.Image#_flagTexture
* @private
* @property {Boolean} - Determines whether the {@link Two.Image#texture} needs updating.
*/
_flagTexture = false;
/**
* @name Two.Image#_flagMode
* @private
* @property {Boolean} - Determines whether the {@link Two.Image#mode} needs updating.
*/
_flagMode = false;
/**
* @name Two.Image#_texture
* @private
* @see {@link Two.Image#texture}
*/
_texture = null;
/**
* @name Two.Image#_mode
* @private
* @see {@link Two.Image#mode}
*/
_mode = 'fill';
constructor(src, ox, oy, width, height, mode) {
super(ox, oy, width || 1, height || 1);
this._renderer.type = 'image';
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this.noStroke();
this.noFill();
/**
* @name Two.Image#texture
* @property {Two.Texture} - The texture to be used as bitmap data to display image in the scene.
*/
if (src instanceof Texture) {
this.texture = src;
} else if (typeof src === 'string') {
this.texture = new Texture(src);
}
if (typeof mode === 'string') {
this.mode = mode;
}
this._update();
}
/**
* @name Two.Image.Modes
* @property {Object} Modes - Different mode types to render an image inspired by Figma.
* @property {String} Modes.fill - Scale image to fill the bounds while preserving aspect ratio.
* @property {String} Modes.fit - Scale image to fit within bounds while preserving aspect ratio.
* @property {String} Modes.crop - Scale image to fill bounds while preserving aspect ratio, cropping excess.
* @property {String} Modes.tile - Repeat image at original size to fill the bounds.
* @property {String} Modes.stretch - Stretch image to fill dimensions, ignoring aspect ratio.
*/
static Modes = {
fill: 'fill',
fit: 'fit',
crop: 'crop',
tile: 'tile',
stretch: 'stretch',
};
/**
* @name Two.Image.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Image}.
*/
static Properties = ['texture', 'mode'];
/**
* @name Two.Image.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Image} to create a new instance
* @returns {Two.Image}
* @description Create a new {@link Two.Image} from an object notation of a {@link Two.Image}.
* @nota-bene Works in conjunction with {@link Two.Image#toObject}
*/
static fromObject(obj) {
const image = new Image().copy(obj);
if ('id' in obj) {
image.id = obj.id;
}
return image;
}
/**
* @name Two.Image#copy
* @function
* @param {Two.Image} image - The reference {@link Two.Image}
* @description Copy the properties of one {@link Two.Image} onto another.
*/
copy(image) {
super.copy.call(this, image);
for (let i = 0; i < Image.Properties.length; i++) {
const k = Image.Properties[i];
if (k in image) {
this[k] = image[k];
}
}
return this;
}
/**
* @name Two.Image#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Image}
* @description Create a new instance of {@link Two.Image} with the same properties of the current image.
*/
clone(parent) {
const clone = new Image(
this.texture,
this.translation.x,
this.translation.y,
this.width,
this.height
);
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Image#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the image.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = 'image';
object.texture = this.texture.toObject();
object.mode = this.mode;
return object;
}
/**
* @name Two.Image#dispose
* @function
* @returns {Two.Image}
* @description Release the image's renderer resources and detach all events.
* This method disposes the texture (calling dispose() for thorough cleanup) and inherits comprehensive
* cleanup from the Rectangle/Path hierarchy while preserving the renderer type
* for potential re-attachment.
*/
dispose() {
// Call parent dispose for inherited cleanup (vertices, fill/stroke effects)
super.dispose();
// Dispose texture (more thorough than unbind)
if (this._texture && typeof this._texture.dispose === 'function') {
this._texture.dispose();
} else if (this._texture && typeof this._texture.unbind === 'function') {
this._texture.unbind();
}
return this;
}
/**
* @name Two.Image#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
const effect = this._texture;
if (effect) {
if (this._flagTexture) {
this.fill = effect;
}
if (effect.loaded) {
const iw = effect.image.width;
const ih = effect.image.height;
const rw = this.width;
const rh = this.height;
// Calculate base scale ratios
const scaleX = rw / iw;
const scaleY = rh / ih;
// Apply scaling based on mode
switch (this._mode) {
case Image.Modes.fill: {
// Fill within bounds while preserving aspect ratio
const scale = Math.max(scaleX, scaleY);
effect.scale = scale;
effect.offset.x = 0;
effect.offset.y = 0;
effect.repeat = 'repeat';
break;
}
case Image.Modes.fit: {
// Fit within bounds while preserving aspect ratio
const scale = Math.min(scaleX, scaleY);
effect.scale = scale; // TODO: For SVG this works `new Vector(scaleX, scaleY);`
effect.offset.x = 0;
effect.offset.y = 0;
effect.repeat = 'no-repeat';
break;
}
case Image.Modes.crop: {
// Intentionally left blank to allow
// external developer to control
break;
}
case Image.Modes.tile: {
// Repeat image and align it correctly
effect.offset.x = (iw - rw) / 2;
effect.offset.y = (ih - rh) / 2;
effect.repeat = 'repeat';
break;
}
case Image.Modes.stretch:
default: {
// Stretch the image texture to whatever the dimensions of the rect are
effect.scale = new Vector(scaleX, scaleY);
effect.offset.x = 0;
effect.offset.y = 0;
effect.repeat = 'repeat';
}
}
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Image#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
super.flagReset.call(this);
this._flagTexture = this._flagMode = false;
return this;
}
}
const proto = {
texture: {
enumerable: true,
get: function () {
return this._texture;
},
set: function (v) {
this._texture = v;
this._flagTexture = true;
},
},
mode: {
enumerable: true,
get: function () {
return this._mode;
},
set: function (v) {
this._mode = v;
this._flagMode = true;
},
},
};
================================================
FILE: src/effects/linear-gradient.d.ts
================================================
declare module 'two.js/src/effects/linear-gradient' {
/**
* @name Two.LinearGradient
* @class
* @param {Number} [x1=0] - The x position of the first end point of the linear gradient.
* @param {Number} [y1=0] - The y position of the first end point of the linear gradient.
* @param {Number} [x2=0] - The x position of the second end point of the linear gradient.
* @param {Number} [y2=0] - The y position of the second end point of the linear gradient.
* @param {Stop[]} [stops] - A list of {@link Two.Stop}s that contain the gradient fill pattern for the gradient.
* @nota-bene The linear gradient lives within the space of the parent object's matrix space.
*/
export class LinearGradient extends Gradient {
/**
* @name Two.LinearGradient.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.LinearGradient} to create a new instance
* @returns {Two.LinearGradient}
* @description Create a new {@link Two.LinearGradient} from an object notation of a {@link Two.LinearGradient}.
* @nota-bene Works in conjunction with {@link Two.LinearGradient#toObject}
*/
static fromObject(
obj: Parameters[0] & {
left?: { x: number; y: number } | Vector;
right?: { x: number; y: number } | Vector;
}
): LinearGradient;
constructor(
x1?: number,
y1?: number,
x2?: number,
y2?: number,
stops?: Stop[]
);
/**
* @name Two.LinearGradient#_flagEndPoints
* @private
* @property {Boolean} - Determines whether the {@link Two.LinearGradient#left} or {@link Two.LinearGradient#right} changed and needs to update.
*/
private _flagEndPoints;
private _left: Vector;
private _right: Vector;
/**
* @name Two.LinearGradient#left
* @property {Vector} - The x and y value for where the first end point is placed on the canvas.
*/
left: Vector;
/**
* @name Two.LinearGradient#right
* @property {Vector} - The x and y value for where the second end point is placed on the canvas.
*/
right: Vector;
/**
* @name Two.LinearGradient#copy
* @function
* @param {Two.LinearGradient} gradient - The reference {@link Two.LinearGradient}
* @description Copy the properties of one {@link Two.LinearGradient} onto another.
*/
copy(gradient: LinearGradient): LinearGradient;
/**
* @name Two.LinearGradient#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Gradient}
* @description Create a new instance of {@link Two.LinearGradient} with the same properties of the current path.
*/
clone(parent?: Group): LinearGradient;
/**
* @name Two.LinearGradient#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject(): object;
/**
* @name Two.LinearGradient#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
protected _update(bubbles?: boolean): LinearGradient;
/**
* @name Two.LinearGradient#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset(): LinearGradient;
}
import { Gradient } from 'two.js/src/effects/gradient';
import { Group } from 'two.js/src/group';
import { Stop } from 'two.js/src/effects/stop';
import { Vector } from 'two.js/src/vector';
}
================================================
FILE: src/effects/linear-gradient.js
================================================
import { Events } from '../events.js';
import { _ } from '../utils/underscore.js';
import { Stop } from './stop.js';
import { Gradient } from './gradient.js';
import { Vector } from '../vector.js';
/**
* @name Two.LinearGradient
* @class
* @extends Two.Gradient
* @param {Number} [x1=0] - The x position of the first end point of the linear gradient.
* @param {Number} [y1=0] - The y position of the first end point of the linear gradient.
* @param {Number} [x2=0] - The x position of the second end point of the linear gradient.
* @param {Number} [y2=0] - The y position of the second end point of the linear gradient.
* @param {Two.Stop[]} [stops] - A list of {@link Two.Stop}s that contain the gradient fill pattern for the gradient.
* @nota-bene The linear gradient lives within the space of the parent object's matrix space.
*/
export class LinearGradient extends Gradient {
/**
* @name Two.LinearGradient#_flagEndPoints
* @private
* @property {Boolean} - Determines whether the {@link Two.LinearGradient#left} or {@link Two.LinearGradient#right} changed and needs to update.
*/
_flagEndPoints = false;
_left = null;
_right = null;
constructor(x1, y1, x2, y2, stops) {
super(stops);
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this._renderer.type = 'linear-gradient';
this._renderer.flagEndPoints = FlagEndPoints.bind(this);
/**
* @name Two.LinearGradient#left
* @property {Two.Vector} - The x and y value for where the first end point is placed on the canvas.
*/
this.left = new Vector();
/**
* @name Two.LinearGradient#right
* @property {Two.Vector} - The x and y value for where the second end point is placed on the canvas.
*/
this.right = new Vector();
if (typeof x1 === 'number') {
this.left.x = x1;
}
if (typeof y1 === 'number') {
this.left.y = y1;
}
if (typeof x2 === 'number') {
this.right.x = x2;
}
if (typeof y2 === 'number') {
this.right.y = y2;
}
}
/**
* @name Two.LinearGradient.Stop
* @see {@link Two.Stop}
*/
static Stop = Stop;
static Properties = ['left', 'right'];
/**
* @name Two.LinearGradient.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.LinearGradient} to create a new instance
* @returns {Two.LinearGradient}
* @description Create a new {@link Two.LinearGradient} from an object notation of a {@link Two.LinearGradient}.
* @nota-bene Works in conjunction with {@link Two.LinearGradient#toObject}
*/
static fromObject(obj) {
const gradient = new LinearGradient().copy(obj);
if ('id' in obj) {
gradient.id = obj.id;
}
return gradient;
}
/**
* @name Two.LinearGradient#copy
* @function
* @param {Two.LinearGradient} gradient - The reference {@link Two.LinearGradient}
* @description Copy the properties of one {@link Two.LinearGradient} onto another.
*/
copy(gradient) {
super.copy.call(this, gradient);
for (let i = 0; i < LinearGradient.Properties.length; i++) {
const k = LinearGradient.Properties[i];
if (k in gradient) {
this[k] =
gradient[k] instanceof Vector
? gradient[k]
: new Vector().copy(gradient[k]);
}
}
return this;
}
/**
* @name Two.LinearGradient#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Gradient}
* @description Create a new instance of {@link Two.LinearGradient} with the same properties of the current path.
*/
clone(parent) {
const stops = this.stops.map(function (stop) {
return stop.clone();
});
const clone = new LinearGradient(
this.left._x,
this.left._y,
this.right._x,
this.right._y,
stops
);
_.each(
Gradient.Properties,
function (k) {
clone[k] = this[k];
},
this
);
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.LinearGradient#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const result = super.toObject.call(this);
result.left = this.left.toObject();
result.right = this.right.toObject();
return result;
}
/**
* @name Two.LinearGradient#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagEndPoints || this._flagSpread || this._flagStops) {
this.trigger(Events.Types.change);
}
return this;
}
/**
* @name Two.LinearGradient#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagEndPoints = false;
super.flagReset.call(this);
return this;
}
}
const proto = {
left: {
enumerable: true,
get: function () {
return this._left;
},
set: function (v) {
if (this._left instanceof Vector) {
this._left.unbind(Events.Types.change, this._renderer.flagEndPoints);
}
this._left = v;
this._left.bind(Events.Types.change, this._renderer.flagEndPoints);
this._flagEndPoints = true;
},
},
right: {
enumerable: true,
get: function () {
return this._right;
},
set: function (v) {
if (this._right instanceof Vector) {
this._right.unbind(Events.Types.change, this._renderer.flagEndPoints);
}
this._right = v;
this._right.bind(Events.Types.change, this._renderer.flagEndPoints);
this._flagEndPoints = true;
},
},
};
/**
* @name FlagEndPoints
* @private
* @function
* @description Cached method to let renderers know end points have been updated on a {@link Two.LinearGradient}.
*/
function FlagEndPoints() {
this._flagEndPoints = true;
}
================================================
FILE: src/effects/radial-gradient.d.ts
================================================
declare module 'two.js/src/effects/radial-gradient' {
/**
* @name Two.RadialGradient
* @class
* @param {Number} [x=0] - The x position of the origin of the radial gradient.
* @param {Number} [y=0] - The y position of the origin of the radial gradient.
* @param {Number} [radius=0] - The radius of the radial gradient.
* @param {Stop[]} [stops] - A list of {@link Two.Stop}s that contain the gradient fill pattern for the gradient.
* @param {Number} [focalX=0] - The x position of the focal point on the radial gradient.
* @param {Number} [focalY=0] - The y position of the focal point on the radial gradient.
* @nota-bene The radial gradient lives within the space of the parent object's matrix space.
*/
export class RadialGradient extends Gradient {
/**
* @name Two.RadialGradient.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.RadialGradient} to create a new instance
* @returns {Two.RadialGradient}
* @description Create a new {@link Two.RadialGradient} from an object notation of a {@link Two.RadialGradient}.
* @nota-bene Works in conjunction with {@link Two.RadialGradient#toObject}
*/
static fromObject(
obj: Parameters[0] & {
radius?: number;
center?: { x: number; y: number } | Vector;
focal?: { x: number; y: number } | Vector;
}
): RadialGradient;
constructor(
cx?: number,
cy?: number,
r?: number,
stops?: Stop[],
fx?: number,
fy?: number
);
/**
* @name Two.RadialGradient#_flagRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.RadialGradient#radius} changed and needs to update.
*/
private _flagRadius;
/**
* @name Two.RadialGradient#_flagCenter
* @private
* @property {Boolean} - Determines whether the {@link Two.RadialGradient#center} changed and needs to update.
*/
private _flagCenter;
/**
* @name Two.RadialGradient#_flagFocal
* @private
* @property {Boolean} - Determines whether the {@link Two.RadialGradient#focal} changed and needs to update.
*/
private _flagFocal;
private _radius: number;
private _center: Vector;
private _focal: Vector;
/**
* @name Two.RadialGradient#center
* @property {Vector} - The x and y value for where the origin of the radial gradient is.
*/
center: Vector;
radius: number;
/**
* @name Two.RadialGradient#focal
* @property {Vector} - The x and y value for where the focal point of the radial gradient is.
* @nota-bene This effects the spray or spread of the radial gradient.
*/
focal: Vector;
/**
* @name Two.RadialGradient#copy
* @function
* @param {Two.RadialGradient} gradient - The reference {@link Two.RadialGradient}
* @description Copy the properties of one {@link Two.RadialGradient} onto another.
*/
copy(gradient: RadialGradient): RadialGradient;
/**
* @name Two.RadialGradient#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.RadialGradient}
* @description Create a new instance of {@link Two.RadialGradient} with the same properties of the current path.
*/
clone(parent?: Group): RadialGradient;
/**
* @name Two.RadialGradient#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject(): object;
/**
* @name Two.RadialGradient#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update(): RadialGradient;
/**
* @name Two.RadialGradient#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset(): RadialGradient;
}
import { Gradient } from 'two.js/src/effects/gradient';
import { Group } from 'two.js/src/group';
import { Stop } from 'two.js/src/effects/stop';
import { Vector } from 'two.js/src/vector';
}
================================================
FILE: src/effects/radial-gradient.js
================================================
import { Events } from '../events.js';
import { _ } from '../utils/underscore.js';
import { Stop } from './stop.js';
import { Gradient } from './gradient.js';
import { Vector } from '../vector.js';
/**
* @name Two.RadialGradient
* @class
* @extends Two.Gradient
* @param {Number} [x=0] - The x position of the origin of the radial gradient.
* @param {Number} [y=0] - The y position of the origin of the radial gradient.
* @param {Number} [radius=0] - The radius of the radial gradient.
* @param {Two.Stop[]} [stops] - A list of {@link Two.Stop}s that contain the gradient fill pattern for the gradient.
* @param {Number} [focalX=0] - The x position of the focal point on the radial gradient.
* @param {Number} [focalY=0] - The y position of the focal point on the radial gradient.
* @nota-bene The radial gradient lives within the space of the parent object's matrix space.
*/
export class RadialGradient extends Gradient {
/**
* @name Two.RadialGradient#_flagRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.RadialGradient#radius} changed and needs to update.
*/
_flagRadius = false;
/**
* @name Two.RadialGradient#_flagCenter
* @private
* @property {Boolean} - Determines whether the {@link Two.RadialGradient#center} changed and needs to update.
*/
_flagCenter = false;
/**
* @name Two.RadialGradient#_flagFocal
* @private
* @property {Boolean} - Determines whether the {@link Two.RadialGradient#focal} changed and needs to update.
*/
_flagFocal = false;
_radius = 0;
_center = null;
_focal = null;
constructor(cx, cy, r, stops, fx, fy) {
super(stops);
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this._renderer.type = 'radial-gradient';
this._renderer.flagCenter = FlagCenter.bind(this);
this._renderer.flagFocal = FlagFocal.bind(this);
/**
* @name Two.RadialGradient#center
* @property {Two.Vector} - The x and y value for where the origin of the radial gradient is.
*/
this.center = new Vector();
this.radius = typeof r === 'number' ? r : 1;
/**
* @name Two.RadialGradient#focal
* @property {Two.Vector} - The x and y value for where the focal point of the radial gradient is.
* @nota-bene This effects the spray or spread of the radial gradient.
*/
this.focal = new Vector();
if (typeof cx === 'number') {
this.center.x = cx;
}
if (typeof cy === 'number') {
this.center.y = cy;
}
this.focal.copy(this.center);
if (typeof fx === 'number') {
this.focal.x = fx;
}
if (typeof fy === 'number') {
this.focal.y = fy;
}
}
/**
* @name Two.RadialGradient.Stop
* @see {@link Two.Stop}
*/
static Stop = Stop;
/**
* @name Two.RadialGradient.Properties
* @property {String[]} - A list of properties that are on every {@link Two.RadialGradient}.
*/
static Properties = ['center', 'radius', 'focal'];
/**
* @name Two.RadialGradient.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.RadialGradient} to create a new instance
* @returns {Two.RadialGradient}
* @description Create a new {@link Two.RadialGradient} from an object notation of a {@link Two.RadialGradient}.
* @nota-bene Works in conjunction with {@link Two.RadialGradient#toObject}
*/
static fromObject(obj) {
const gradient = new RadialGradient().copy(obj);
if ('id' in obj) {
gradient.id = obj.id;
}
return gradient;
}
/**
* @name Two.RadialGradient#copy
* @function
* @param {Two.RadialGradient} gradient - The reference {@link Two.RadialGradient}
* @description Copy the properties of one {@link Two.RadialGradient} onto another.
*/
copy(gradient) {
super.copy.call(this, gradient);
for (let i = 0; i < RadialGradient.Properties.length; i++) {
const k = RadialGradient.Properties[i];
if (k in gradient) {
if (/(center|focal)i/.test(k)) {
this[k] =
gradient[k] instanceof Vector
? gradient[k]
: new Vector().copy(gradient[k]);
} else if (typeof gradient[k] === 'number') {
this[k] = gradient[MediaKeySystemAccess];
}
}
}
return this;
}
/**
* @name Two.RadialGradient#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.RadialGradient}
* @description Create a new instance of {@link Two.RadialGradient} with the same properties of the current path.
*/
clone(parent) {
const stops = this.stops.map(function (stop) {
return stop.clone();
});
const clone = new RadialGradient(
this.center._x,
this.center._y,
this._radius,
stops,
this.focal._x,
this.focal._y
);
_.each(
Gradient.Properties.concat(RadialGradient.Properties),
function (k) {
clone[k] = this[k];
},
this
);
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.RadialGradient#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const result = super.toObject.call(this);
_.each(
RadialGradient.Properties,
function (k) {
result[k] = this[k];
},
this
);
result.center = this.center.toObject();
result.focal = this.focal.toObject();
return result;
}
/**
* @name Two.RadialGradient#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (
this._flagRadius ||
this._flatCenter ||
this._flagFocal ||
this._flagSpread ||
this._flagStops
) {
this.trigger(Events.Types.change);
}
return this;
}
/**
* @name Two.RadialGradient#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagRadius = this._flagCenter = this._flagFocal = false;
super.flagReset.call(this);
return this;
}
}
const proto = {
radius: {
enumerable: true,
get: function () {
return this._radius;
},
set: function (v) {
this._radius = v;
this._flagRadius = true;
},
},
center: {
enumerable: true,
get: function () {
return this._center;
},
set: function (v) {
if (this._center) {
this._center.unbind(Events.Types.change, this._renderer.flagCenter);
}
this._center = v;
this._center.bind(Events.Types.change, this._renderer.flagCenter);
this._flagCenter = true;
},
},
focal: {
enumerable: true,
get: function () {
return this._focal;
},
set: function (v) {
if (this._focal) {
this._focal.unbind(Events.Types.change, this._renderer.flagFocal);
}
this._focal = v;
this._focal.bind(Events.Types.change, this._renderer.flagFocal);
this._flagFocal = true;
},
},
};
function FlagCenter() {
this._flagCenter = true;
}
function FlagFocal() {
this._flagFocal = true;
}
================================================
FILE: src/effects/sprite.d.ts
================================================
declare module 'two.js/src/effects/sprite' {
/**
* @name Two.Sprite
* @class
* @param {String|Texture} [src] - The URL path or {@link Two.Texture} to be used as the bitmap data displayed on the sprite.
* @param {Number} [ox=0] - The initial `x` position of the Two.Sprite.
* @param {Number} [oy=0] - The initial `y` position of the Two.Sprite.
* @param {Number} [cols=1] - The number of columns the sprite contains.
* @param {Number} [rows=1] - The number of rows the sprite contains.
* @param {Number} [frameRate=0] - The frame rate at which the partitions of the image should playback at.
* @description A convenient package to display still or animated images through a tiled image source. For more information on the principals of animated imagery through tiling see [Texture Atlas](https://en.wikipedia.org/wiki/Texture_atlas) on Wikipedia.
*/
export class Sprite extends Rectangle {
/**
* @name Two.Sprite.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Sprite}.
*/
static Properties: (
| 'texture'
| 'columns'
| 'rows'
| 'frameRate'
| 'index'
| 'firstFrame'
| 'lastFrame'
| 'loop'
| string
)[];
/**
* @name Two.Sprite.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Sprite} to create a new instance
* @returns {Two.Sprite}
* @description Create a new {@link Two.Sprite} from an object notation of a {@link Two.Sprite}.
* @nota-bene Works in conjunction with {@link Two.Sprite#toObject}
*/
static fromObject(
obj: Parameters[0] & {
texture?: Parameters[0];
columns?: number;
rows?: number;
frameRate?: number;
index?: number;
firstFrame?: number;
lastFrame?: number;
loop?: boolean;
}
): Sprite;
constructor(
src?: string | Texture,
ox?: number,
oy?: number,
cols?: number,
rows?: number,
frameRate?: number
);
/**
* @name Two.Sprite#_flagTexture
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#texture} needs updating.
*/
private _flagTexture;
/**
* @name Two.Sprite#_flagColumns
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#columns} need updating.
*/
private _flagColumns;
/**
* @name Two.Sprite#_flagRows
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#rows} need updating.
*/
private _flagRows;
/**
* @name Two.Sprite#_flagFrameRate
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#flagFrameRate} needs updating.
*/
private _flagFrameRate;
/**
* @name Two.Sprite#_flagIndex
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#index} needs updating.
*/
private _flagIndex;
/**
* @name Two.Sprite#_amount
* @private
* @property {Number} - Number of frames for a given {@link Two.Sprite}.
*/
private _amount;
/**
* @name Two.Sprite#_duration
* @private
* @property {Number} - Number of milliseconds a {@link Two.Sprite}.
*/
private _duration;
/**
* @name Two.Sprite#_startTime
* @private
* @property {Milliseconds} - Epoch time in milliseconds of when the {@link Two.Sprite} started.
*/
private _startTime;
/**
* @name Two.Sprite#_playing
* @private
* @property {Boolean} - Dictates whether the {@link Two.Sprite} is animating or not.
*/
private _playing;
/**
* @name Two.Sprite#_firstFrame
* @private
* @property {Number} - The frame the {@link Two.Sprite} should start with.
*/
private _firstFrame;
/**
* @name Two.Sprite#_lastFrame
* @private
* @property {Number} - The frame the {@link Two.Sprite} should end with.
*/
private _lastFrame;
/**
* @name Two.Sprite#_playing
* @private
* @property {Boolean} - Dictates whether the {@link Two.Sprite} should loop or not.
*/
private _loop;
/**
* @name Two.Sprite#_texture
* @private
* @see {@link Two.Sprite#texture}
*/
private _texture;
/**
* @name Two.Sprite#_columns
* @private
* @see {@link Two.Sprite#columns}
*/
private _columns;
/**
* @name Two.Sprite#_rows
* @private
* @see {@link Two.Sprite#rows}
*/
private _rows;
/**
* @name Two.Sprite#_frameRate
* @private
* @see {@link Two.Sprite#frameRate}
*/
private _frameRate;
/**
* @name Two.Sprite#_index
* @private
* @property {Number} - The current frame the {@link Two.Sprite} is currently displaying.
*/
private _index;
texture: Texture;
columns: number;
rows: number;
frameRate: number;
/**
* @name Two.Sprite#index
* @property {Number} - The index of the current tile of the sprite to display. Defaults to `0`.
*/
index: number;
/**
* @name Two.Sprite#copy
* @function
* @param {Two.Sprite} sprite - The reference {@link Two.Sprite}
* @description Copy the properties of one {@link Two.Sprite} onto another.
*/
copy(sprite: Sprite): Sprite;
/**
* @name Two.Sprite#play
* @function
* @param {Number} [firstFrame=0] - The index of the frame to start the animation with.
* @param {Number} [lastFrame] - The index of the frame to end the animation with. Defaults to the last item in the {@link Two.Sprite#textures}.
* @param {Function} [onLastFrame] - Optional callback function to be triggered after playing the last frame. This fires multiple times when the sprite is looped.
* @description Initiate animation playback of a {@link Two.Sprite}.
*/
play(
firstFrame?: number,
lastFrame?: number,
onLastFrame?: () => void
): Sprite;
/**
* @name Two.Sprite#pause
* @function
* @description Halt animation playback of a {@link Two.Sprite}.
*/
pause(): Sprite;
/**
* @name Two.Sprite#stop
* @function
* @description Halt animation playback of a {@link Two.Sprite} and set the current frame back to the first frame.
*/
stop(): Sprite;
/**
* @name Two.Sprite#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Sprite}
* @description Create a new instance of {@link Two.Sprite} with the same properties of the current sprite.
*/
clone(parent?: Group): Sprite;
/**
* @name Two.Sprite#dispose
* @function
* @description Release the sprite's renderer resources and detach all events.
* This method stops any running animation, clears animation callbacks, disposes
* the texture (calling dispose() for thorough cleanup), and inherits comprehensive
* cleanup from the Rectangle/Path hierarchy while preserving the renderer type
* for potential re-attachment.
*/
dispose(): Sprite;
}
import { Rectangle } from 'two.js/src/shapes/rectangle';
import { Texture } from 'two.js/src/effects/texture';
import { Group } from 'two.js/src/group';
}
================================================
FILE: src/effects/sprite.js
================================================
import { lerp } from '../utils/math.js';
import { _ } from '../utils/underscore.js';
import { Vector } from '../vector.js';
import { Rectangle } from '../shapes/rectangle.js';
import { Texture } from './texture.js';
/**
* @name Two.Sprite
* @class
* @extends Two.Rectangle
* @param {String|Two.Texture} [src] - The URL path or {@link Two.Texture} to be used as the bitmap data displayed on the sprite.
* @param {Number} [ox=0] - The initial `x` position of the Two.Sprite.
* @param {Number} [oy=0] - The initial `y` position of the Two.Sprite.
* @param {Number} [cols=1] - The number of columns the sprite contains.
* @param {Number} [rows=1] - The number of rows the sprite contains.
* @param {Number} [frameRate=0] - The frame rate at which the partitions of the image should playback at.
* @description A convenient package to display still or animated images through a tiled image source. For more information on the principals of animated imagery through tiling see [Texture Atlas](https://en.wikipedia.org/wiki/Texture_atlas) on Wikipedia.
*/
export class Sprite extends Rectangle {
/**
* @name Two.Sprite#_flagTexture
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#texture} needs updating.
*/
_flagTexture = false;
/**
* @name Two.Sprite#_flagColumns
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#columns} need updating.
*/
_flagColumns = false;
/**
* @name Two.Sprite#_flagRows
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#rows} need updating.
*/
_flagRows = false;
/**
* @name Two.Sprite#_flagFrameRate
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#flagFrameRate} needs updating.
*/
_flagFrameRate = false;
/**
* @name Two.Sprite#_flagIndex
* @private
* @property {Boolean} - Determines whether the {@link Two.Sprite#index} needs updating.
*/
_flagIndex = false;
// Private variables
/**
* @name Two.Sprite#_amount
* @private
* @property {Number} - Number of frames for a given {@link Two.Sprite}.
*/
_amount = 1;
/**
* @name Two.Sprite#_duration
* @private
* @property {Number} - Number of milliseconds a {@link Two.Sprite}.
*/
_duration = 0;
/**
* @name Two.Sprite#_startTime
* @private
* @property {Milliseconds} - Epoch time in milliseconds of when the {@link Two.Sprite} started.
*/
_startTime = 0;
/**
* @name Two.Sprite#_playing
* @private
* @property {Boolean} - Dictates whether the {@link Two.Sprite} is animating or not.
*/
_playing = false;
/**
* @name Two.Sprite#_firstFrame
* @private
* @property {Number} - The frame the {@link Two.Sprite} should start with.
*/
_firstFrame = 0;
/**
* @name Two.Sprite#_lastFrame
* @private
* @property {Number} - The frame the {@link Two.Sprite} should end with.
*/
_lastFrame = 0;
/**
* @name Two.Sprite#_loop
* @private
* @property {Boolean} - Dictates whether the {@link Two.Sprite} should loop or not.
*/
_loop = true;
// Exposed through getter-setter
/**
* @name Two.Sprite#_texture
* @private
* @see {@link Two.Sprite#texture}
*/
_texture = null;
/**
* @name Two.Sprite#_columns
* @private
* @see {@link Two.Sprite#columns}
*/
_columns = 1;
/**
* @name Two.Sprite#_rows
* @private
* @see {@link Two.Sprite#rows}
*/
_rows = 1;
/**
* @name Two.Sprite#_frameRate
* @private
* @see {@link Two.Sprite#frameRate}
*/
_frameRate = 0;
/**
* @name Two.Sprite#_index
* @private
* @property {Number} - The current frame the {@link Two.Sprite} is currently displaying.
*/
_index = 0;
/**
* @name Two.Sprite#_origin
* @private
* @see {@link Two.Sprite#origin}
*/
_origin = null;
constructor(src, ox, oy, cols, rows, frameRate) {
super(ox, oy, 0, 0);
this._renderer.type = 'sprite';
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this.noStroke();
this.noFill();
/**
* @name Two.Sprite#texture
* @property {Two.Texture} - The texture to be used as bitmap data to display image in the scene.
*/
if (src instanceof Texture) {
this.texture = src;
} else if (typeof src === 'string') {
this.texture = new Texture(src);
}
this.origin = new Vector();
this._update();
/**
* @name Two.Sprite#columns
* @property {Number} - The number of columns to split the texture into. Defaults to `1`.
*/
if (typeof cols === 'number') {
this.columns = cols;
}
/**
* @name Two.Sprite#rows
* @property {Number} - The number of rows to split the texture into. Defaults to `1`.
*/
if (typeof rows === 'number') {
this.rows = rows;
}
/**
* @name Two.Sprite#frameRate
* @property {Number} - The number of frames to animate against per second. Defaults to `0` for non-animated sprites.
*/
if (typeof frameRate === 'number') {
this.frameRate = frameRate;
}
/**
* @name Two.Sprite#index
* @property {Number} - The index of the current tile of the sprite to display. Defaults to `0`.
*/
this.index = 0;
}
/**
* @name Two.Sprite.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Sprite}.
*/
static Properties = [
'texture',
'columns',
'rows',
'frameRate',
'index',
'firstFrame',
'lastFrame',
'loop',
];
/**
* @name Two.Sprite.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Sprite} to create a new instance
* @returns {Two.Sprite}
* @description Create a new {@link Two.Sprite} from an object notation of a {@link Two.Sprite}.
* @nota-bene Works in conjunction with {@link Two.Sprite#toObject}
*/
static fromObject(obj) {
const sprite = new Sprite().copy(obj);
if ('id' in obj) {
sprite.id = obj.id;
}
return sprite;
}
/**
* @name Two.Sprite#copy
* @function
* @param {Two.Sprite} sprite - The reference {@link Two.Sprite}
* @description Copy the properties of one {@link Two.Sprite} onto another.
*/
copy(sprite) {
super.copy.call(this, sprite);
for (let i = 0; i < Sprite.Properties.length; i++) {
const k = Sprite.Properties[i];
if (k in sprite) {
this[k] = sprite[k];
}
}
return this;
}
/**
* @name Two.Sprite#play
* @function
* @param {Number} [firstFrame=0] - The index of the frame to start the animation with.
* @param {Number} [lastFrame] - The index of the frame to end the animation with. Defaults to the last item in the {@link Two.Sprite#textures}.
* @param {Function} [onLastFrame] - Optional callback function to be triggered after playing the last frame. This fires multiple times when the sprite is looped.
* @description Initiate animation playback of a {@link Two.Sprite}.
*/
play(firstFrame, lastFrame, onLastFrame) {
this._playing = true;
this._firstFrame = 0;
this._lastFrame = this.amount - 1;
this._startTime = _.performance.now();
if (typeof firstFrame === 'number') {
this._firstFrame = firstFrame;
}
if (typeof lastFrame === 'number') {
this._lastFrame = lastFrame;
}
if (typeof onLastFrame === 'function') {
this._onLastFrame = onLastFrame;
} else {
delete this._onLastFrame;
}
if (this._index !== this._firstFrame) {
this._startTime -=
(1000 * Math.abs(this._index - this._firstFrame)) / this._frameRate;
}
return this;
}
/**
* @name Two.Sprite#pause
* @function
* @description Halt animation playback of a {@link Two.Sprite}.
*/
pause() {
this._playing = false;
return this;
}
/**
* @name Two.Sprite#stop
* @function
* @description Halt animation playback of a {@link Two.Sprite} and set the current frame back to the first frame.
*/
stop() {
this._playing = false;
this._index = 0;
return this;
}
/**
* @name Two.Sprite#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Sprite}
* @description Create a new instance of {@link Two.Sprite} with the same properties of the current sprite.
*/
clone(parent) {
const clone = new Sprite(
this.texture,
this.translation.x,
this.translation.y,
this.columns,
this.rows,
this.frameRate
);
if (this.playing) {
clone.play(this._firstFrame, this._lastFrame);
}
clone.loop = this.loop;
clone.firstFrame = this.firstFrame;
clone.lastFrame = this.lastFrame;
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Sprite#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = 'sprite';
object.texture = this.texture.toObject();
object.columns = this.columns;
object.rows = this.rows;
object.frameRate = this.frameRate;
object.index = this.index;
object.firstFrame = this.firstFrame;
object.lastFrame = this.lastFrame;
object.loop = this.loop;
return object;
}
/**
* @name Two.Sprite#dispose
* @function
* @returns {Two.Sprite}
* @description Release the sprite's renderer resources and detach all events.
* This method stops any running animation, clears animation callbacks, disposes
* the texture (calling dispose() for thorough cleanup), and inherits comprehensive
* cleanup from the Rectangle/Path hierarchy while preserving the renderer type
* for potential re-attachment.
*/
dispose() {
// Call parent dispose for inherited cleanup (vertices, fill/stroke effects)
super.dispose();
// Stop animation if playing
if (this._playing) {
this._playing = false;
}
// Clear animation callbacks
this._onLastFrame = null;
// Reset timing properties
this._startTime = 0;
// Dispose texture (more thorough than unbind)
if (this._texture && typeof this._texture.dispose === 'function') {
this._texture.dispose();
} else if (this._texture && typeof this._texture.unbind === 'function') {
this._texture.unbind();
}
return this;
}
/**
* @name Two.Sprite#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
const effect = this._texture;
const cols = this._columns;
const rows = this._rows;
let width, height, elapsed, amount, duration;
let index, iw, ih, frames;
if (effect) {
if (this._flagColumns || this._flagRows) {
this._amount = this._columns * this._rows;
}
if (this._flagFrameRate) {
this._duration = (1000 * this._amount) / this._frameRate;
}
if (this._flagTexture) {
this.fill = effect;
}
if (effect.loaded) {
iw = effect.image.width;
ih = effect.image.height;
width = iw / cols;
height = ih / rows;
amount = this._amount;
if (this.width !== width) {
this.width = width;
}
if (this.height !== height) {
this.height = height;
}
if (this._playing && this._frameRate > 0) {
if (_.isNaN(this._lastFrame)) {
this._lastFrame = amount - 1;
}
// TODO: Offload perf logic to instance of `Two`.
elapsed = _.performance.now() - this._startTime;
frames = this._lastFrame + 1;
duration = (1000 * (frames - this._firstFrame)) / this._frameRate;
if (this._loop) {
elapsed = elapsed % duration;
} else {
elapsed = Math.min(elapsed, duration);
}
index = lerp(this._firstFrame, frames, elapsed / duration);
index = Math.floor(index);
if (index !== this._index) {
this._index = index;
if (index >= this._lastFrame - 1 && this._onLastFrame) {
this._onLastFrame(); // Shortcut for chainable sprite animations
}
}
}
const col = this._index % cols;
const row = Math.floor(this._index / cols);
const ox = -width * col + (iw - width) / 2;
const oy = -height * row + (ih - height) / 2;
// TODO: Improve performance
if (ox !== effect.offset.x) {
effect.offset.x = ox;
}
if (oy !== effect.offset.y) {
effect.offset.y = oy;
}
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Sprite#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagTexture =
this._flagColumns =
this._flagRows =
this._flagFrameRate =
false;
super.flagReset.call(this);
return this;
}
}
const proto = {
texture: {
enumerable: true,
get: function () {
return this._texture;
},
set: function (v) {
this._texture = v;
this._flagTexture = true;
},
},
columns: {
enumerable: true,
get: function () {
return this._columns;
},
set: function (v) {
this._columns = v;
this._flagColumns = true;
},
},
rows: {
enumerable: true,
get: function () {
return this._rows;
},
set: function (v) {
this._rows = v;
this._flagRows = true;
},
},
frameRate: {
enumerable: true,
get: function () {
return this._frameRate;
},
set: function (v) {
this._frameRate = v;
this._flagFrameRate = true;
},
},
index: {
enumerable: true,
get: function () {
return this._index;
},
set: function (v) {
this._index = v;
this._flagIndex = true;
},
},
firstFrame: {
enumerable: true,
get: function () {
return this._firstFrame;
},
set: function (v) {
this._firstFrame = v;
},
},
lastFrame: {
enumerable: true,
get: function () {
return this._lastFrame;
},
set: function (v) {
this._lastFrame = v;
},
},
loop: {
enumerable: true,
get: function () {
return this._loop;
},
set: function (v) {
this._loop = !!v;
},
},
};
================================================
FILE: src/effects/stop.d.ts
================================================
declare module 'two.js/src/effects/stop' {
/**
* @name Two.Stop
* @class
* @param {Number} [offset] - The offset percentage of the stop represented as a zero-to-one value. Default value flip flops from zero-to-one as new stops are created.
* @param {String} [color] - The color of the stop. Default value flip flops from white to black as new stops are created.
* @param {Number} [opacity] - The opacity value. Default value is 1, cannot be lower than 0.
* @nota-bene Used specifically in conjunction with {@link Two.Gradient}s to control color graduation.
*/
export class Stop extends TwoElement {
/**
* @name Two.Stop.Index
* @property {Number} - The current index being referenced for calculating a stop's default offset value.
*/
static Index: number;
/**
* @name Two.Stop.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Stop}.
*/
static override Properties: ('offset' | 'color' | 'opacity' | string)[];
/**
* @name Two.Stop.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Stop} to create a new instance
* @returns {Two.Stop}
* @description Create a new {@link Two.Stop} from an object notation of a {@link Two.Stop}.
* @nota-bene Works in conjunction with {@link Two.Stop#toObject}
*/
static fromObject(
obj: Parameters[0] & {
offset?: number;
color?: string;
opacity?: number;
}
): Stop;
constructor(offset?: number, color?: string, opacity?: number);
/**
* @name Two.Stop#_flagOffset
* @private
* @property {Boolean} - Determines whether the {@link Two.Stop#offset} needs updating.
*/
private _flagOffset;
/**
* @name Two.Stop#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Stop#opacity} needs updating.
*/
private _flagOpacity;
/**
* @name Two.Stop#_flagColor
* @private
* @property {Boolean} - Determines whether the {@link Two.Stop#color} needs updating.
*/
private _flagColor;
/**
* @name Two.Stop#_offset
* @private
* @see {@link Two.Stop#offset}
*/
private _offset;
/**
* @name Two.Stop#_opacity
* @private
* @see {@link Two.Stop#opacity}
*/
private _opacity;
/**
* @name Two.Stop#_color
* @private
* @see {@link Two.Stop#color}
*/
private _color;
/**
* @name Two.Stop#offset
* @property {Number} - The offset percentage of the stop represented as a zero-to-one value.
*/
offset: number;
/**
* @name Two.Stop#opacity
* @property {Number} - The alpha percentage of the stop represented as a zero-to-one value.
*/
opacity: number;
/**
* @name Two.Stop#color
* @property {String} - The color of the stop.
*/
color: string;
/**
* @name Two.Stop#copy
* @function
* @param {Two.Stop} stop - The reference {@link Two.Stop}
* @description Copy the properties of one {@link Two.Stop} onto another.
*/
copy(stop: Stop): Stop;
/**
* @name Two.Stop#clone
* @function
* @param {Gradient} [parent] - The parent group or scene to add the clone to.
* @returns {Stop}
* @description Create a new instance of {@link Two.Stop} with the same properties of the current path.
*/
clone(parent?: Gradient): Stop;
/**
* @name Two.Stop#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject(): object;
/**
* @name Two.Stop#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset(): Stop;
}
import { Element as TwoElement } from 'two.js/src/element';
import { Gradient } from 'two.js/src/effects/gradient';
}
================================================
FILE: src/effects/stop.js
================================================
import { _ } from '../utils/underscore.js';
import { Element } from '../element.js';
/**
* @name Two.Stop
* @class
* @extends Two.Element
* @param {Number} [offset] - The offset percentage of the stop represented as a zero-to-one value. Default value flip flops from zero-to-one as new stops are created.
* @param {String} [color] - The color of the stop. Default value flip flops from white to black as new stops are created.
* @param {Number} [opacity] - The opacity value. Default value is 1, cannot be lower than 0.
* @nota-bene Used specifically in conjunction with {@link Two.Gradient}s to control color graduation.
*/
export class Stop extends Element {
/**
* @name Two.Stop#_flagOffset
* @private
* @property {Boolean} - Determines whether the {@link Two.Stop#offset} needs updating.
*/
_flagOffset = true;
/**
* @name Two.Stop#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Stop#opacity} needs updating.
*/
_flagOpacity = true;
/**
* @name Two.Stop#_flagColor
* @private
* @property {Boolean} - Determines whether the {@link Two.Stop#color} needs updating.
*/
_flagColor = true;
/**
* @name Two.Stop#_offset
* @private
* @see {@link Two.Stop#offset}
*/
_offset = 0;
/**
* @name Two.Stop#_opacity
* @private
* @see {@link Two.Stop#opacity}
*/
_opacity = 1;
/**
* @name Two.Stop#_color
* @private
* @see {@link Two.Stop#color}
*/
_color = '#fff';
constructor(offset, color, opacity) {
super();
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this._renderer.type = 'stop';
/**
* @name Two.Stop#offset
* @property {Number} - The offset percentage of the stop represented as a zero-to-one value.
*/
this.offset = typeof offset === 'number' ? offset : Stop.Index <= 0 ? 0 : 1;
/**
* @name Two.Stop#opacity
* @property {Number} - The alpha percentage of the stop represented as a zero-to-one value.
* @nota-bene This is only supported on the {@link Two.SVGRenderer}. You can get the same effect by encoding opacity into `rgba` strings in the color.
*/
this.opacity = typeof opacity === 'number' ? opacity : 1;
/**
* @name Two.Stop#color
* @property {String} - The color of the stop.
*/
this.color =
typeof color === 'string' ? color : Stop.Index <= 0 ? '#fff' : '#000';
Stop.Index = (Stop.Index + 1) % 2;
}
/**
* @name Two.Stop.Index
* @property {Number} - The current index being referenced for calculating a stop's default offset value.
*/
static Index = 0;
/**
* @name Two.Stop.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Stop}.
*/
static Properties = ['offset', 'opacity', 'color'];
/**
* @name Two.Stop.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Stop} to create a new instance
* @returns {Two.Stop}
* @description Create a new {@link Two.Stop} from an object notation of a {@link Two.Stop}.
* @nota-bene Works in conjunction with {@link Two.Stop#toObject}
*/
static fromObject(obj) {
const stop = new Stop().copy(obj);
if ('id' in obj) {
stop.id = obj.id;
}
return stop;
}
/**
* @name Two.Stop#copy
* @function
* @param {Two.Stop} stop - The reference {@link Two.Stop}
* @description Copy the properties of one {@link Two.Stop} onto another.
*/
copy(stop) {
super.copy.call(this, stop);
for (let i = 0; i < Stop.Properties.length; i++) {
const k = Stop.Properties[i];
if (k in stop) {
this[k] = stop[k];
}
}
return this;
}
/**
* @name Two.Stop#clone
* @function
* @param {Two.Gradient} [parent] - The parent gradient to add the clone to.
* @returns {Two.Stop}
* @description Create a new instance of {@link Two.Stop} with the same properties of the current path.
*/
clone(parent) {
const clone = new Stop();
_.each(
Stop.Properties,
function (property) {
clone[property] = this[property];
},
this
);
if (parent && parent.stops) {
parent.stops.push(clone);
}
return clone;
}
/**
* @name Two.Stop#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = 'stop';
_.each(
Stop.Properties,
(k) => {
result[k] = this[k];
},
this
);
return result;
}
/**
* @name Two.Stop#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagOffset = this._flagColor = this._flagOpacity = false;
super.flagReset.call(this);
return this;
}
}
const proto = {
offset: {
enumerable: true,
get: function () {
return this._offset;
},
set: function (v) {
this._offset = v;
this._flagOffset = true;
if (this.parent) {
this.parent._flagStops = true;
}
},
},
opacity: {
enumerable: true,
get: function () {
return this._opacity;
},
set: function (v) {
this._opacity = v;
this._flagOpacity = true;
if (this.parent) {
this.parent._flagStops = true;
}
},
},
color: {
enumerable: true,
get: function () {
return this._color;
},
set: function (v) {
this._color = v;
this._flagColor = true;
if (this.parent) {
this.parent._flagStops = true;
}
},
},
};
================================================
FILE: src/effects/texture.d.ts
================================================
declare module 'two.js/src/effects/texture' {
type RepeatProperties = 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat';
/**
* @name Two.Texture
* @class
* @extends Two.Element
* @param {String|HTMLImageElement} [src] - The URL path to an image file or an `` element.
* @param {Function} [callback] - An optional callback function once the image has been loaded.
* @description Fundamental to work with bitmap data, a.k.a. pregenerated imagery, in Two.js. Supported formats include jpg, png, gif, and tiff. See {@link Two.Texture.RegularExpressions} for a full list of supported formats.
*/
export class Texture extends TwoElement {
/**
* @name Two.Texture.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Texture}.
*/
static Properties: string[];
/**
* @name Two.Texture.RegularExpressions
* @property {Object} - A map of compatible DOM Elements categorized by media format.
*/
static RegularExpressions: {
video: RegExp;
image: RegExp;
effect: RegExp;
};
/**
* @name Two.Texture.ImageRegistry
* @property {Registry} - A canonical listing of image data used in a single session of Two.js.
* @nota-bene This object is used to cache image data between different textures.
*/
static ImageRegistry: Registry;
/**
* @name Two.Texture.getAbsoluteURL
* @property {Function} - Serializes a URL as an absolute path for canonical attribution in {@link Two.ImageRegistry}.
* @param {String} path
* @returns {String} - The serialized absolute path.
*/
static getAbsoluteURL(path: string): string;
/**
* @name Two.Texture.loadHeadlessBuffer
* @property {Function} - Loads an image as a buffer in headless environments.
* @param {Texture} texture - The {@link Two.Texture} to be loaded.
* @param {Function} onLoad - The callback function to be triggered once the image is loaded.
* @nota-bene - This function uses node's `fs.readFileSync` to spoof the `` loading process in the browser.
*/
static loadHeadlessBuffer(texture: Texture, onLoad: Function): void;
/**
* @name Two.Texture.getTag
* @property {Function} - Retrieves the tag name of an image, video, or canvas node.
* @param {HTMLImageElement} image - The image to infer the tag name from.
* @returns {String} - Returns the tag name of an image, video, or canvas node.
*/
static getTag(image: HTMLImageElement): string;
/**
* @name Two.Texture.getImage
* @property {Function} - Convenience function to set {@link Two.Texture#image} properties with canonincal versions set in {@link Two.Texture.ImageRegistry}.
* @param {String} src - The URL path of the image.
* @returns {HTMLImageElement} - Returns either a cached version of the image or a new one that is registered in {@link Two.Texture.ImageRegistry}.
*/
static getImage(src: string): HTMLImageElement;
/**
* @name Two.Texture.Register
* @interface
* @description A collection of functions to register different types of textures. Used internally by a {@link Two.Texture}.
*/
static Register: {
canvas: (texture: Texture, callback: Function) => void;
img: (texture: Texture, callback: Function) => void;
video: (texture: Texture, callback: Function) => void;
};
/**
* @name Two.Texture.load
* @function
* @param {Texture} texture - The texture to load.
* @param {Function} callback - The function to be called once the texture is loaded.
*/
static load(texture: Texture, callback: Function): void;
/**
* @name Two.Texture.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Texture} to create a new instance
* @returns {Two.Texture}
* @description Create a new {@link Two.Texture} from an object notation of a {@link Two.Texture}.
* @nota-bene Works in conjunction with {@link Two.Texture#toObject}
*/
static fromObject(
obj: Parameters[0] & {
src?: string;
repeat?: RepeatProperties;
offset?: { x: number; y: number } | Vector;
scale?: { x: number; y: number } | Vector;
}
): Texture;
constructor(
src?: string | HTMLImageElement | HTMLCanvasElement | HTMLVideoElement,
callback?: Function
);
/**
* @name Two.Texture#_flagSrc
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#src} needs updating.
*/
private _flagSrc;
/**
* @name Two.Texture#_flagImage
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#image} needs updating.
*/
private _flagImage;
/**
* @name Two.Texture#_flagVideo
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#video} needs updating.
*/
private _flagVideo;
/**
* @name Two.Texture#_flagLoaded
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#loaded} needs updating.
*/
private _flagLoaded;
/**
* @name Two.Texture#_flagRepeat
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#repeat} needs updating.
*/
private _flagRepeat;
/**
* @name Two.Texture#_flagOffset
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#offset} needs updating.
*/
private _flagOffset;
/**
* @name Two.Texture#_flagScale
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#scale} needs updating.
*/
private _flagScale;
/**
* @name Two.Texture#_src
* @private
* @see {@link Two.Texture#src}
*/
private _src;
/**
* @name Two.Texture#_image
* @private
* @see {@link Two.Texture#image}
*/
private _image;
/**
* @name Two.Texture#_loaded
* @private
* @see {@link Two.Texture#loaded}
*/
private _loaded;
/**
* @name Two.Texture#_repeat
* @private
* @see {@link Two.Texture#repeat}
*/
private _repeat;
/**
* @name Two.Texture#_scale
* @private
* @see {@link Two.Texture#scale}
*/
private _scale;
/**
* @name Two.Texture#_offset
* @private
* @see {@link Two.Texture#offset}
*/
private _offset;
id: string;
/**
* @name Two.Texture#loaded
* @property {Boolean} - Shorthand value to determine if image has been loaded into the texture.
*/
loaded: boolean;
/**
* @name Two.Texture#repeat
* @property {String} - CSS style declaration to tile {@link Two.Path}. Valid values include: `'no-repeat'`, `'repeat'`, `'repeat-x'`, `'repeat-y'`.
* @see {@link https://www.w3.org/TR/2dcontext/#dom-context-2d-createpattern}
*/
repeat: RepeatProperties;
/**
* @name Two.Texture#offset
* @property {Vector} - A two-component vector describing any pixel offset of the texture when applied to a {@link Two.Path}.
*/
offset: Vector;
src: string;
/**
* @name Two.Texture#image
* @property {Element} - The corresponding DOM Element of the texture. Can be a ``, ``, or `` element. See {@link Two.Texture.RegularExpressions} for a full list of supported elements.
* @nota-bene In headless environments this is a `Canvas.Image` object. See {@link https://github.com/Automattic/node-canvas} for more information on headless image objects.
*/
image: HTMLImageElement | HTMLCanvasElement | HTMLVideoElement;
/**
* @name Two.Texture#clone
* @function
* @returns {Texture}
* @description Create a new instance of {@link Two.Texture} with the same properties of the current texture.
*/
clone(): Texture;
/**
* @name Two.Texture#copy
* @function
* @param {Two.Texture} texture - The reference {@link Two.Texture}
* @description Copy the properties of one {@link Two.Texture} onto another.
*/
copy(texture: Texture): Texture;
/**
* @name Two.Texture#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the texture.
*/
toObject(): object;
/**
* @name Two.Texture#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
protected _update(bubbles?: boolean): Texture;
/**
* @name Two.Texture#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset(): Texture;
/**
* @name Two.Gradient#dispose
* @function
* @description Detach instance from renderer including any `` or textures stored in memory.
*/
dispose(): Texture;
}
import { Element as TwoElement } from 'two.js/src/element';
import { Vector } from 'two.js/src/vector';
import { Registry } from 'two.js/src/registry';
}
================================================
FILE: src/effects/texture.js
================================================
import { root } from '../utils/root.js';
import { Events } from '../events.js';
import { Element } from '../element.js';
import { TwoError } from '../utils/error.js';
import { CanvasPolyfill } from '../utils/canvas-polyfill.js';
import { Vector } from '../vector.js';
import { Registry } from '../registry.js';
let anchor;
const regex = {
video: /\.(mp4|webm|ogg)$/i,
image: /\.(jpe?g|png|gif|tiff|webp)$/i,
effect: /texture|gradient/i,
};
if (root.document) {
anchor = document.createElement('a');
}
/**
* @name Two.Texture
* @class
* @extends Two.Element
* @param {String|HTMLImageElement} [src] - The URL path to an image file or an `` element.
* @param {Function} [callback] - An optional callback function once the image has been loaded.
* @description Fundamental to work with bitmap data, a.k.a. pregenerated imagery, in Two.js. Supported formats include jpg, png, gif, and tiff. See {@link Two.Texture.RegularExpressions} for a full list of supported formats.
*/
export class Texture extends Element {
/**
* @name Two.Texture#_flagSrc
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#src} needs updating.
*/
_flagSrc = false;
/**
* @name Two.Texture#_flagImage
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#image} needs updating.
*/
_flagImage = false;
/**
* @name Two.Texture#_flagVideo
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#video} needs updating.
*/
_flagVideo = false;
/**
* @name Two.Texture#_flagLoaded
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#loaded} needs updating.
*/
_flagLoaded = false;
/**
* @name Two.Texture#_flagRepeat
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#repeat} needs updating.
*/
_flagRepeat = false;
/**
* @name Two.Texture#_flagOffset
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#offset} needs updating.
*/
_flagOffset = false;
/**
* @name Two.Texture#_flagScale
* @private
* @property {Boolean} - Determines whether the {@link Two.Texture#scale} needs updating.
*/
_flagScale = false;
/**
* @name Two.Texture#_src
* @private
* @see {@link Two.Texture#src}
*/
_src = '';
/**
* @name Two.Texture#_image
* @private
* @see {@link Two.Texture#image}
*/
_image = null;
/**
* @name Two.Texture#_loaded
* @private
* @see {@link Two.Texture#loaded}
*/
_loaded = false;
/**
* @name Two.Texture#_repeat
* @private
* @see {@link Two.Texture#repeat}
*/
_repeat = 'no-repeat';
/**
* @name Two.Texture#_scale
* @private
* @see {@link Two.Texture#scale}
*/
_scale = 1;
/**
* @name Two.Texture#_offset
* @private
* @see {@link Two.Texture#offset}
*/
_offset = null;
constructor(src, callback) {
super();
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this._renderer.type = 'texture';
this._renderer.flagOffset = FlagOffset.bind(this);
this._renderer.flagScale = FlagScale.bind(this);
/**
* @name Two.Texture#loaded
* @property {Boolean} - Shorthand value to determine if image has been loaded into the texture.
*/
this.loaded = false;
/**
* @name Two.Texture#repeat
* @property {String} - CSS style declaration to tile {@link Two.Path}. Valid values include: `'no-repeat'`, `'repeat'`, `'repeat-x'`, `'repeat-y'`.
* @see {@link https://www.w3.org/TR/2dcontext/#dom-context-2d-createpattern}
*/
this.repeat = 'no-repeat';
/**
* @name Two.Texture#offset
* @property {Two.Vector} - A two-component vector describing any pixel offset of the texture when applied to a {@link Two.Path}.
*/
this.offset = new Vector();
if (typeof callback === 'function') {
const loaded = function () {
this.unbind(Events.Types.load, loaded);
if (typeof callback === 'function') {
callback();
}
}.bind(this);
this.bind(Events.Types.load, loaded);
}
/**
* @name Two.Texture#src
* @property {String} - The URL path to the image data.
* @nota-bene This property is ultimately serialized in a {@link Two.Registry} to cache retrieval.
*/
if (typeof src === 'string') {
this.src = src;
} else if (typeof src === 'object') {
const elemString = Object.prototype.toString.call(src);
if (
elemString === '[object HTMLImageElement]' ||
elemString === '[object HTMLCanvasElement]' ||
elemString === '[object HTMLVideoElement]' ||
elemString === '[object Image]'
) {
/**
* @name Two.Texture#image
* @property {Element} - The corresponding DOM Element of the texture. Can be a ``, ``, or `` element. See {@link Two.Texture.RegularExpressions} for a full list of supported elements.
* @nota-bene In headless environments this is a `Canvas.Image` object. See {@link https://github.com/Automattic/node-canvas} for more information on headless image objects.
*/
this.image = src;
}
}
this._update();
}
/**
* @name Two.Texture.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Texture}.
*/
static Properties = ['src', 'loaded', 'repeat', 'scale', 'offset', 'image'];
/**
* @name Two.Texture.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Texture} to create a new instance
* @returns {Two.Texture}
* @description Create a new {@link Two.Texture} from an object notation of a {@link Two.Texture}.
* @nota-bene Works in conjunction with {@link Two.Texture#toObject}
*/
fromObject(obj) {
const texture = new Texture().copy(obj);
if ('id' in obj) {
texture.id = obj.id;
}
return texture;
}
/**
* @name Two.Texture.RegularExpressions
* @property {Object} - A map of compatible DOM Elements categorized by media format.
*/
static RegularExpressions = regex;
/**
* @name Two.Texture.ImageRegistry
* @property {Two.Registry} - A canonical listing of image data used in a single session of Two.js.
* @nota-bene This object is used to cache image data between different textures.
*/
static ImageRegistry = new Registry();
/**
* @name Two.Texture.getAbsoluteURL
* @property {Function} - Serializes a URL as an absolute path for canonical attribution in {@link Two.Texture.ImageRegistry}.
* @param {String} path
* @returns {String} - The serialized absolute path.
*/
static getAbsoluteURL(path) {
if (!anchor) {
// TODO: Fix for headless environments
return path;
}
anchor.href = path;
return anchor.href;
}
/**
* @name Two.Texture.loadHeadlessBuffer
* @property {Function} - Loads an image as a buffer in headless environments.
* @param {Two.Texture} texture - The {@link Two.Texture} to be loaded.
* @param {Function} onLoad - The callback function to be triggered once the image is loaded.
* @nota-bene - This function uses node's `fs.readFileSync` to spoof the `` loading process in the browser.
*/
static loadHeadlessBuffer(texture, onLoad) {
texture.image.onload = onLoad;
texture.image.src = texture.src;
}
/**
* @name Two.Texture.getTag
* @property {Function} - Retrieves the tag name of an image, video, or canvas node.
* @param {HTMLImageElement} image - The image to infer the tag name from.
* @returns {String} - Returns the tag name of an image, video, or canvas node.
*/
static getTag(image) {
return (
(image && image.nodeName && image.nodeName.toLowerCase()) ||
// Headless environments
'img'
);
}
/**
* @name Two.Texture.getImage
* @property {Function} - Convenience function to set {@link Two.Texture#image} properties with canonical versions set in {@link Two.Texture.ImageRegistry}.
* @param {String} src - The URL path of the image.
* @returns {HTMLImageElement} - Returns either a cached version of the image or a new one that is registered in {@link Two.Texture.ImageRegistry}.
*/
static getImage(src) {
const absoluteSrc = Texture.getAbsoluteURL(src);
if (Texture.ImageRegistry.contains(absoluteSrc)) {
return Texture.ImageRegistry.get(absoluteSrc);
}
let image;
if (CanvasPolyfill.Image) {
// TODO: Fix for headless environments
image = new CanvasPolyfill.Image();
CanvasPolyfill.shim(image, 'img');
} else if (root.document) {
if (regex.video.test(absoluteSrc)) {
image = document.createElement('video');
} else {
image = document.createElement('img');
}
} else {
console.warn('Two.js: no prototypical image defined for Two.Texture');
}
image.crossOrigin = 'anonymous';
image.referrerPolicy = 'no-referrer';
return image;
}
/**
* @name Two.Texture.Register
* @interface
* @description A collection of functions to register different types of textures. Used internally by a {@link Two.Texture}.
*/
static Register = {
canvas: function (texture, callback) {
texture._src = '#' + texture.id;
Texture.ImageRegistry.add(texture.src, texture.image);
if (typeof callback === 'function') {
callback();
}
},
img: function (texture, callback) {
const image = texture.image;
const loaded = function (e) {
if (
!CanvasPolyfill.isHeadless &&
image.removeEventListener &&
typeof image.removeEventListener === 'function'
) {
image.removeEventListener('load', loaded, false);
image.removeEventListener('error', error, false);
}
if (typeof callback === 'function') {
callback();
}
};
const error = function (e) {
if (
!CanvasPolyfill.isHeadless &&
typeof image.removeEventListener === 'function'
) {
image.removeEventListener('load', loaded, false);
image.removeEventListener('error', error, false);
}
throw new TwoError('unable to load ' + texture.src);
};
if (
typeof image.width === 'number' &&
image.width > 0 &&
typeof image.height === 'number' &&
image.height > 0
) {
loaded();
} else if (
!CanvasPolyfill.isHeadless &&
typeof image.addEventListener === 'function'
) {
image.addEventListener('load', loaded, false);
image.addEventListener('error', error, false);
}
texture._src = Texture.getAbsoluteURL(texture._src);
if (
!CanvasPolyfill.isHeadless &&
image &&
image.getAttribute('two-src')
) {
return;
}
if (!CanvasPolyfill.isHeadless) {
image.setAttribute('two-src', texture.src);
}
Texture.ImageRegistry.add(texture.src, image);
if (CanvasPolyfill.isHeadless) {
Texture.loadHeadlessBuffer(texture, loaded);
} else {
texture.image.src = texture.src;
}
},
video: function (texture, callback) {
if (CanvasPolyfill.isHeadless) {
throw new TwoError(
'video textures are not implemented in headless environments.'
);
}
const loaded = function (e) {
texture.image.removeEventListener('canplaythrough', loaded, false);
texture.image.removeEventListener('error', error, false);
texture.image.width = texture.image.videoWidth;
texture.image.height = texture.image.videoHeight;
if (typeof callback === 'function') {
callback();
}
};
const error = function (e) {
texture.image.removeEventListener('canplaythrough', loaded, false);
texture.image.removeEventListener('error', error, false);
throw new TwoError('unable to load ' + texture.src);
};
texture._src = Texture.getAbsoluteURL(texture._src);
if (!texture.image.getAttribute('two-src')) {
texture.image.setAttribute('two-src', texture.src);
Texture.ImageRegistry.add(texture.src, texture.image);
}
if (texture.image.readyState >= 4) {
loaded();
} else {
texture.image.addEventListener('canplaythrough', loaded, false);
texture.image.addEventListener('error', error, false);
texture.image.src = texture.src;
texture.image.load();
}
},
};
/**
* @name Two.Texture.load
* @function
* @param {Two.Texture} texture - The texture to load.
* @param {Function} callback - The function to be called once the texture is loaded.
*/
static load(texture, callback) {
let image = texture.image;
let tag = Texture.getTag(image);
if (texture._flagImage) {
if (/canvas/i.test(tag)) {
Texture.Register.canvas(texture, callback);
} else {
texture._src =
(!CanvasPolyfill.isHeadless && image.getAttribute('two-src')) ||
image.src;
Texture.Register[tag](texture, callback);
}
}
if (texture._flagSrc) {
if (!image) {
image = Texture.getImage(texture.src);
texture.image = image;
}
tag = Texture.getTag(image);
Texture.Register[tag](texture, callback);
}
}
/**
* @name Two.Texture#clone
* @function
* @returns {Two.Texture}
* @description Create a new instance of {@link Two.Texture} with the same properties of the current texture.
*/
clone() {
const clone = new Texture(this.src);
clone.repeat = this.repeat;
clone.offset.copy(this.offset);
clone.scale = this.scale;
return clone;
}
/**
* @name Two.Texture#copy
* @function
* @param {Two.Texture} texture - The reference {@link Two.Texture}
* @description Copy the properties of one {@link Two.Texture} onto another.
*/
copy(texture) {
this.src = texture.src;
this.repeat = texture.repeat;
this.offset =
typeof texture.offset === 'number' || texture.offset instanceof Vector
? texture.offset
: new Vector().copy(texture.offset);
this.scale =
typeof texture.scale === 'number' || texture.scale instanceof Vector
? texture.scale
: new Vector().copy(texture.scale);
return this;
}
/**
* @name Two.Texture#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the texture.
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = 'texture';
result.src = this.src;
result.repeat = this.repeat;
result.offset = this.offset.toObject();
result.scale =
typeof this.scale === 'number' ? this.scale : this.scale.toObject();
return result;
}
/**
* @name Two.Texture#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagSrc || this._flagImage) {
this.trigger(Events.Types.change);
if (this._flagSrc || this._flagImage) {
this.loaded = false;
Texture.load(
this,
function () {
this.loaded = true;
this.trigger(Events.Types.change).trigger(Events.Types.load);
}.bind(this)
);
}
}
if (this._image && this._image.readyState >= 4) {
this._flagVideo = true;
}
return this;
}
/**
* @name Two.Texture#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagSrc =
this._flagImage =
this._flagLoaded =
this._flagRepeat =
this._flagVideo =
this._flagScale =
this._flagOffset =
false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Texture#dispose
* @function
* @description Detach instance from renderer including any `` or textures stored in memory.
*/
dispose() {
super.dispose();
// Remove gradient from SVG document
if ('elem' in this._renderer) {
const elem = this._renderer.elem;
elem.parentNode.removeChild(elem);
}
// Deallocate textures from the graphics card
if ('effect' in this._renderer) {
this._renderer.effect = null;
}
return this;
}
}
const proto = {
src: {
enumerable: true,
get: function () {
return this._src;
},
set: function (v) {
this._src = v;
this._flagSrc = true;
},
},
loaded: {
enumerable: true,
get: function () {
return this._loaded;
},
set: function (v) {
this._loaded = v;
this._flagLoaded = true;
},
},
repeat: {
enumerable: true,
get: function () {
return this._repeat;
},
set: function (v) {
this._repeat = v;
this._flagRepeat = true;
},
},
image: {
enumerable: true,
get: function () {
return this._image;
},
set: function (image) {
const tag = Texture.getTag(image);
let index;
switch (tag) {
case 'canvas':
index = '#' + image.id;
break;
default:
index = image.src;
}
if (Texture.ImageRegistry.contains(index)) {
this._image = Texture.ImageRegistry.get(image.src);
} else {
this._image = image;
}
this._flagImage = true;
},
},
offset: {
enumerable: true,
get: function () {
return this._offset;
},
set: function (v) {
if (this._offset) {
this._offset.unbind(Events.Types.change, this._renderer.flagOffset);
}
this._offset = v;
this._offset.bind(Events.Types.change, this._renderer.flagOffset);
this._flagOffset = true;
},
},
scale: {
enumerable: true,
get: function () {
return this._scale;
},
set: function (v) {
if (this._scale instanceof Vector) {
this._scale.unbind(Events.Types.change, this._renderer.flagScale);
}
this._scale = v;
if (this._scale instanceof Vector) {
this._scale.bind(Events.Types.change, this._renderer.flagScale);
}
this._flagScale = true;
},
},
};
/**
* @name Two.Texture.FlagOffset
* @function
* @description Cached method to let renderers know `offset` has been updated on a {@link Two.Texture}.
*/
function FlagOffset() {
this._flagOffset = true;
}
/**
* @name Two.Texture.FlagScale
* @function
* @description Cached method to let renderers know `scale` has been updated on a {@link Two.Texture}.
*/
function FlagScale() {
this._flagScale = true;
}
================================================
FILE: src/element.d.ts
================================================
declare module 'two.js/src/element' {
/**
* @name Two.Element
* @class
* @description The foundational object for the Two.js scenegraph.
*/
export class Element extends Events {
static Properties: string[];
/**
* @name Two.Element.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Element} to create a new instance
* @returns {Two.Element}
* @description Create a new {@link Two.Element} from an object notation of a {@link Two.Element}.
* @nota-bene Works in conjunction with {@link Two.Element#toObject}
*/
static fromObject(
obj:
| object
| {
renderer?: { type: string };
id?: string;
className?: string;
}
): Element;
/**
* @name Two.Element#_flagId
* @private
* @property {Boolean} - Determines whether the {@link Two.Element#id} needs updating.
*/
private _flagId;
/**
* @name Two.Element#_flagClassName
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#className} need updating.
*/
private _flagClassName;
/**
* @name Two.Element#renderer
* @property {Object} - Object access to store relevant renderer specific variables. Warning: manipulating this object can create unintended consequences.
* @nota-bene With the {@link Two.SVGRenderer} you can access the underlying SVG element created via `shape.renderer.elem`.
*/
renderer: {
type: 'element' | 'group' | 'path' | 'text' | 'points' | string;
elem?:
| SVGGElement
| SVGPathElement
| SVGTextElement
| SVGPatternElement
| SVGDefsElement
| SVGGradientElement
| SVGLinearGradientElement
| SVGRadialGradientElement
| SVGImageElement
| SVGClipPathElement
| SVGStopElement
| SVGPatternElement;
onBeforeRender?: () => void;
onAfterRender?: () => void;
};
/**
* @name Two.Element#id
* @property {String} - Session specific unique identifier.
* @nota-bene In the {@link Two.SVGRenderer} change this to change the underlying SVG element's id too.
*/
id: string;
/**
* @name Two.Element#className
* @property {String} - A class to be applied to the element to be compatible with CSS styling.
* @nota-bene Only available for the SVG renderer.
*/
className: string;
/**
* @name Two.Element#classList
* @property {String[]}
* @description A list of class strings stored if imported / interpreted from an SVG element.
*/
classList: string[];
/**
* @name Two.Element#flagReset
* @function
* @description Called internally by Two.js's renderer to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset(): Element;
copy(element: Element): Element;
toObject(): object;
/**
* @name Two.Element#dispose
* @function
* @description Release the element's renderer object and detach any events.
* This cleans up renderer-specific resources and unbinds all event listeners.
*/
dispose(): Element;
}
import { Events } from 'two.js/src/events';
}
================================================
FILE: src/element.js
================================================
import { Events } from './events.js';
import { Constants } from './constants.js';
/**
* @name Two.Element
* @class
* @extends Two.Events
* @description The foundational object for the Two.js scenegraph.
*/
export class Element extends Events {
/**
* @name Two.Element#_flagId
* @private
* @property {Boolean} - Determines whether the {@link Two.Element#id} needs updating.
*/
_flagId = false;
/**
* @name Two.Element#_flagClassName
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#className} need updating.
*/
_flagClassName = false;
/**
* @name Two.Element#renderer
* @property {Object} - Object access to store relevant renderer specific variables. Warning: manipulating this object can create unintended consequences.
* @nota-bene With the {@link Two.SVGRenderer} you can access the underlying SVG element created via `shape.renderer.elem`.
*/
_renderer = {};
/**
* @name Two.Element#id
* @property {String} - Session specific unique identifier.
* @nota-bene In the {@link Two.SVGRenderer} change this to change the underlying SVG element's id too.
*/
_id = Constants.Identifier + Constants.uniqueId();
/**
* @name Two.Element#className
* @property {String} - A class to be applied to the element to be compatible with CSS styling.
* @nota-bene Only rendered to DOM in the SVG renderer.
*/
_className = '';
/**
* @name Two.Element#classList
* @property {String[]}
* @description A list of class strings stored if imported / interpreted from an SVG element.
*/
classList = [];
constructor() {
super();
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
}
static Properties = ['renderer', 'id', 'className'];
/**
* @name Two.Element.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Element} to create a new instance
* @returns {Two.Element}
* @description Create a new {@link Two.Element} from an object notation of a {@link Two.Element}.
* @nota-bene Works in conjunction with {@link Two.Element#toObject}
*/
static fromObject(obj) {
const elem = new Element().copy(obj);
if ('id' in obj) {
elem.id = obj.id;
}
return elem;
}
/**
* @name Two.Element#flagReset
* @function
* @description Called internally by Two.js's renderer to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagId = this._flagClassName = false;
return this;
}
copy(element) {
// Explicitly do not copy the id
// of an object to keep uniqueness
if (element.renderer && typeof element.renderer.type === 'string') {
this.renderer.type = element.renderer.type;
}
if (typeof element.className === 'string') {
this.className = element.className;
}
return this;
}
toObject() {
return {
renderer: { type: this.renderer.type },
id: this.id,
className: this.className,
};
}
/**
* @name Two.Element#dispose
* @function
* @description Release the element's renderer object and detach any events.
* This cleans up renderer-specific resources and unbinds all event listeners.
*/
dispose() {
// Unbind all events
if (typeof this.unbind === 'function') {
this.unbind();
}
// Clean up renderer-specific resources
if (this._renderer) {
// SVG DOM element cleanup
if (this._renderer.elem && this._renderer.elem.parentNode) {
this._renderer.elem.parentNode.removeChild(this._renderer.elem);
delete this._renderer.elem;
}
// WebGL resource cleanup
if (this.type === 'WebGLRenderer' && this.renderer.ctx) {
const gl = this.renderer.ctx;
// Clean up textures
if (this._renderer.texture) {
gl.deleteTexture(this._renderer.texture);
delete this._renderer.texture;
}
// Clean up buffers
if (this._renderer.positionBuffer) {
gl.deleteBuffer(this._renderer.positionBuffer);
delete this._renderer.positionBuffer;
}
// Clean up any other WebGL effects
if (this._renderer.effect) {
this._renderer.effect = null;
}
}
// Canvas renderer cleanup - clear cached contexts and data
if (this.type === 'CanvasRenderer' && this._renderer.context) {
delete this._renderer.context;
}
}
// Preserve the renderer type for potential re-attachment
const rendererType = this._renderer.type;
// Clear renderer object but preserve type
this._renderer = { type: rendererType };
return this;
}
}
const proto = {
renderer: {
enumerable: false,
get: function () {
return this._renderer;
},
},
id: {
enumerable: true,
get: function () {
return this._id;
},
set: function (v) {
const id = this._id;
if (v === this._id) {
return;
}
this._id = v;
this._flagId = true;
if (this.parent) {
delete this.parent.children.ids[id];
this.parent.children.ids[this._id] = this;
}
},
},
className: {
enumerable: true,
get: function () {
return this._className;
},
set: function (v) {
if (this._className !== v) {
this._flagClassName = true;
this.classList = v.split(/\s+?/);
this._className = v;
}
},
},
};
================================================
FILE: src/events.d.ts
================================================
declare module 'two.js/src/events' {
/**
* @name Two.Events
* @class
* @description Object inherited by many Two.js objects in order to facilitate custom events.
*/
export class Events {
/**
* @name Two.Events.Types
* @property {Object} - Object of different types of Two.js specific events.
*/
static Types: {
play: 'play';
pause: 'pause';
update: 'update';
render: 'render';
resize: 'resize';
change: 'change';
remove: 'remove';
insert: 'insert';
order: 'order';
load: 'load';
};
static Methods: (
| 'addEventListener'
| 'on'
| 'removeEventListener'
| 'off'
| 'unbind'
| 'dispatchEvent'
| 'trigger'
| 'listen'
| 'ignore'
)[];
private _events: {};
private _bound: boolean;
/**
* @name Two.Events#addEventListener
* @function
* @param {String} [name] - The name of the event to bind a function to.
* @param {Function} [handler] - The function to be invoked when the event is dispatched.
* @description Call to add a listener to a specific event name.
*/
addEventListener(name?: string, handler?: Function): Events;
/**
* @name Two.Events#on
* @function
* @description Alias for {@link Two.Events#addEventListener}.
*/
on(name?: string, handler?: Function): Events;
/**
* @name Two.Events#bind
* @function
* @description Alias for {@link Two.Events#addEventListener}.
*/
bind(name?: string, handler?: Function): Events;
/**
* @name Two.Events#removeEventListener
* @function
* @param {String} [name] - The name of the event intended to be removed.
* @param {Function} [handler] - The handler intended to be reomved.
* @description Call to remove listeners from a specific event. If only `name` is passed then all the handlers attached to that `name` will be removed. If no arguments are passed then all handlers for every event on the obejct are removed.
*/
removeEventListener(name?: string, handler?: Function): Events;
/**
* @name Two.Events#off
* @function
* @description Alias for {@link Two.Events#removeEventListener}.
*/
off(name?: string, handler?: Function): Events;
/**
* @name Two.Events#unbind
* @function
* @description Alias for {@link Two.Events#removeEventListener}.
*/
unbind(name?: string, handler?: Function): Events;
/**
* @name Two.Events#dispatchEvent
* @function
* @param {String} name - The name of the event to dispatch.
* @param args - Anything can be passed after the name and those will be passed on to handlers attached to the event in the order they are passed.
* @description Call to trigger a custom event. Any additional arguments passed after the name will be passed along to the attached handlers.
*/
dispatchEvent(name: string, ...args: any[]): Events;
trigger(...args: any[]): any;
listen(obj: any, name: string, handler: Function): Events;
ignore(obj: any, name: string, handler: Function): Events;
}
}
================================================
FILE: src/events.js
================================================
/**
* @name Two.Events
* @class
* @description Object inherited by many Two.js objects in order to facilitate custom events.
*/
export class Events {
_events = {};
_bound = false;
constructor() {}
/**
* @name Two.Events#addEventListener
* @function
* @param {String} [name] - The name of the event to bind a function to.
* @param {Function} [handler] - The function to be invoked when the event is dispatched.
* @description Call to add a listener to a specific event name.
*/
addEventListener(name, handler) {
const list = this._events[name] || (this._events[name] = []);
list.push(handler);
this._bound = true;
return this;
}
/**
* @name Two.Events#on
* @function
* @description Alias for {@link Two.Events#addEventListener}.
*/
on() {
return this.addEventListener.apply(this, arguments);
}
/**
* @name Two.Events#bind
* @function
* @description Alias for {@link Two.Events#addEventListener}.
*/
bind() {
return this.addEventListener.apply(this, arguments);
}
/**
* @name Two.Events#removeEventListener
* @function
* @param {String} [name] - The name of the event intended to be removed.
* @param {Function} [handler] - The handler intended to be removed.
* @description Call to remove listeners from a specific event. If only `name` is passed then all the handlers attached to that `name` will be removed. If no arguments are passed then all handlers for every event on the obejct are removed.
*/
removeEventListener(name, handler) {
if (!this._events) {
return this;
}
if (!name && !handler) {
this._events = {};
this._bound = false;
return this;
}
const names = name ? [name] : Object.keys(this._events);
for (let i = 0, l = names.length; i < l; i++) {
name = names[i];
let list = this._events[name];
if (list) {
let events = [];
if (handler) {
for (let j = 0, k = list.length; j < k; j++) {
let e = list[j];
e = e.handler ? e.handler : e;
if (handler !== e) {
events.push(e);
}
}
}
this._events[name] = events;
}
}
return this;
}
/**
* @name Two.Events#off
* @function
* @description Alias for {@link Two.Events#removeEventListener}.
*/
off() {
return this.removeEventListener.apply(this, arguments);
}
/**
* @name Two.Events#unbind
* @function
* @description Alias for {@link Two.Events#removeEventListener}.
*/
unbind() {
return this.removeEventListener.apply(this, arguments);
}
/**
* @name Two.Events#dispatchEvent
* @function
* @param {String} name - The name of the event to dispatch.
* @param args - Anything can be passed after the name and those will be passed on to handlers attached to the event in the order they are passed.
* @description Call to trigger a custom event. Any additional arguments passed after the name will be passed along to the attached handlers.
*/
dispatchEvent(name) {
if (!this._events) {
return this;
}
const args = Array.prototype.slice.call(arguments, 1);
const events = this._events[name];
if (events) {
for (let i = 0; i < events.length; i++) {
events[i].call(this, ...args);
}
}
return this;
}
trigger() {
return this.dispatchEvent.apply(this, arguments);
}
listen(obj, name, handler) {
const scope = this;
if (obj) {
// Add references about the object that assigned this listener
e.obj = obj;
e.name = name;
e.handler = handler;
obj.on(name, e);
}
function e() {
handler.apply(scope, arguments);
}
return scope;
}
ignore(obj, name, handler) {
obj.off(name, handler);
return this;
}
/**
* @name Two.Events.Types
* @property {Object} - Object of different types of Two.js specific events.
*/
static Types = {
play: 'play',
pause: 'pause',
update: 'update',
render: 'render',
resize: 'resize',
change: 'change',
remove: 'remove',
insert: 'insert',
order: 'order',
load: 'load'
}
static Methods = [
'addEventListener', 'on', 'removeEventListener', 'off', 'unbind',
'dispatchEvent', 'trigger', 'listen', 'ignore'
];
}
================================================
FILE: src/group.d.ts
================================================
declare module 'two.js/src/group' {
type ChildParams =
| Parameters[0]
| Parameters[0]
| Parameters[0];
/**
* @name Two.Group
* @class
* @param {Shape[]} [children] - A list of objects that inherit {@link Two.Shape}. For instance, the array could be a {@link Two.Path}, {@link Two.Text}, and {@link Two.RoundedRectangle}.
* @description This is the primary class for grouping objects that are then drawn in Two.js. In Illustrator this is a group, in After Effects it would be a Null Object. Whichever the case, the `Two.Group` contains a transformation matrix and commands to style its children, but it by itself doesn't render to the screen.
* @nota-bene The {@link Two#scene} is an instance of `Two.Group`.
*/
export class Group extends Shape {
static Children: Children;
static IsVisible(element: Element, visibleOnly?: boolean): boolean;
static VisitForHitTest(
group: Group,
context: {
x: number;
y: number;
visibleOnly: boolean;
results: Element[];
},
includeGroups: boolean,
filter: Function | null,
hitOptions: Pick<
SceneHitTestOptions,
'precision' | 'fill' | 'stroke' | 'tolerance' | 'ignoreVisibility'
>,
tolerance: number,
stopOnFirst: null
): boolean;
/**
* @name Two.Group.InsertChildren
* @function
* @param {Shape[]} children - The objects to be inserted.
* @description Cached method to let renderers know children have been added to a {@link Two.Group}.
*/
static InsertChildren(children: Shape[]): void;
/**
* @name Two.Group.RemoveChildren
* @function
* @param {Shape[]} children - The objects to be removed.
* @description Cached method to let renderers know children have been removed from a {@link Two.Group}.
*/
static RemoveChildren(children: Shape[]): void;
/**
* @name Two.Group.OrderChildren
* @function
* @description Cached method to let renderers know order has been updated on a {@link Two.Group}.
*/
static OrderChildren(children: Shape[]): void;
/**
* @name Two.Group.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Group}.
*/
static Properties: string[];
static fromObject(
obj: Parameters[0] & {
children?: (ChildParams | Parameters[0])[];
opacity?: number;
mask?: ChildParams;
}
): Group;
constructor(children?: Shape[]);
constructor(...args: Shape[]);
/**
* @name Two.Group#_flagAdditions
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#additions} needs updating.
*/
private _flagAdditions;
/**
* @name Two.Group#_flagSubtractions
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#subtractions} needs updating.
*/
private _flagSubtractions;
/**
* @name Two.Group#_flagOrder
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#order} needs updating.
*/
private _flagOrder;
/**
* @name Two.Group#_flagVisible
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#visible} needs updating.
*/
/**
* @name Two.Group#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#opacity} needs updating.
*/
private _flagOpacity;
/**
* @name Two.Group#_flagBeginning
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#beginning} needs updating.
*/
private _flagBeginning;
/**
* @name Two.Group#_flagEnding
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#ending} needs updating.
*/
private _flagEnding;
/**
* @name Two.Group#_flagLength
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#length} needs updating.
*/
private _flagLength;
/**
* @name Two.Group#_flagMask
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#mask} needs updating.
*/
private _flagMask;
/**
* @name Two.Group#fill
* @property {(String|Gradient|Texture)} - The value of what all child shapes should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
fill: string | Gradient | Texture;
/**
* @name Two.Group#stroke
* @property {(String|Gradient|Texture)} - The value of what all child shapes should be outlined in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
stroke: string | Gradient | Texture;
/**
* @name Two.Group#linewidth
* @property {Number} - The thickness in pixels of the stroke for all child shapes.
*/
linewidth: number;
/**
* @name Two.Group#opacity
* @property {Number} - The opaqueness of all child shapes.
* @nota-bene Becomes multiplied by the individual child's opacity property.
*/
opacity: number;
/**
* @name Two.Group#visible
* @property {Boolean} - Display the path or not.
* @nota-bene For {@link Two.CanvasRenderer} and {@link Two.WebGLRenderer} when set to false all updating is disabled improving performance dramatically with many objects in the scene.
*/
visible: boolean;
/**
* @name Two.Group#cap
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeLinecapProperty}
*/
cap: CapProperties;
/**
* @name Two.Group#join
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeLinejoinProperty}
*/
join: JoinProperties;
/**
* @name Two.Group#miter
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeMiterlimitProperty}
*/
miter: number;
/**
* @name Two.Group#closed
* @property {Boolean} - Determines whether a final line is drawn between the final point in the `vertices` array and the first point of all child shapes.
*/
closed: boolean;
/**
* @name Two.Group#curved
* @property {Boolean} - When the child's path is `automatic = true` this boolean determines whether the lines between the points are curved or not.
*/
curved: boolean;
/**
* @name Two.Group#automatic
* @property {Boolean} - Determines whether or not Two.js should calculate curves, lines, and commands automatically for you or to let the developer manipulate them for themselves.
*/
automatic: boolean;
/**
* @name Two.Group#beginning
* @property {Number} - Number between zero and one to state the beginning of where the path is rendered.
* @description {@link Two.Group#beginning} is a percentage value that represents at what percentage into all child shapes should the renderer start drawing.
* @nota-bene This is great for animating in and out stroked paths in conjunction with {@link Two.Group#ending}.
*/
beginning: number;
/**
* @name Two.Group#ending
* @property {Number} - Number between zero and one to state the ending of where the path is rendered.
* @description {@link Two.Group#ending} is a percentage value that represents at what percentage into all child shapes should the renderer start drawing.
* @nota-bene This is great for animating in and out stroked paths in conjunction with {@link Two.Group#beginning}.
*/
ending: number;
/**
* @name Two.Group#length
* @property {Number} - The sum of distances between all child lengths.
*/
length: number;
/**
* @name Two.Group#mask
* @property {Shape} - The Two.js object to clip from a group's rendering.
*/
mask: Shape | null | undefined;
/**
* @name Two.Group#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space for all child shapes.
* @description When `strokeAttenuation` is `false`, this property is applied to all child shapes, making their stroke widths automatically adjust to compensate for the group's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke widths scale normally with transformations.
*/
strokeAttenuation: boolean;
/**
* @name Two.Group#additions
* @property {Shape[]}
* @description An automatically updated list of children that need to be appended to the renderer's scenegraph.
*/
additions: Shape[];
/**
* @name Two.Group#subtractions
* @property {Shape[]}
* @description An automatically updated list of children that need to be removed from the renderer's scenegraph.
*/
subtractions: Shape[];
/**
* @name Two.Group#children
* @property {Group.Children}
* @description A list of all the children in the scenegraph.
* @nota-bene Ther order of this list indicates the order each element is rendered to the screen.
*/
children: Children;
/**
* @name Two.Group#copy
* @function
* @param {Two.Group} [group] - The reference {@link Two.Group}
* @returns {Two.Group}
* @description Copy the properties of one {@link Two.Group} onto another.
*/
copy(group: Group): Group;
/**
* @name Two.Group#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the group.
*/
toObject(): Object;
/**
* @name Two.Group#dispose
* @function
* @description Release the group's renderer resources and detach all events.
* This method recursively disposes all child objects, unbinds the children
* collection events, and preserves the renderer type for potential re-attachment
* to a new renderer.
*/
dispose(): Group;
/**
* @name Two.Group#getShapesAtPoint
* @function
* @param {Number} x - X coordinate in world space.
* @param {Number} y - Y coordinate in world space.
* @param {SceneHitTestOptions} [options]
* @returns {Shape[]} Ordered list of intersecting shapes, front to back.
* @description Traverse the group hierarchy and return shapes that contain the specified point.
*/
getShapesAtPoint(
x: number,
y: number,
options?: SceneHitTestOptions
): Shape[];
/**
* @name Two.Group#corner
* @function
* @description Orient the children of the group to the upper left-hand corner of that group.
*/
corner(): Group;
/**
* @name Two.Group#center
* @function
* @description Orient the children of the group to the center of that group.
*/
center(): Group;
/**
* @name Two.Group#getById
* @function
* @description Recursively search for id. Returns the first element found.
* @returns {Shape} - Or `null` if nothing is found.
*/
getById(id: string): Shape;
/**
* @name Two.Group#getByClassName
* @function
* @description Recursively search for classes. Returns an array of matching elements.
* @returns {Shape[]} - Or empty array if nothing is found.
*/
getByClassName(className: string): Shape[];
/**
* @name Two.Group#getByType
* @function
* @description Recursively search for children of a specific type, e.g. {@link Two.Path}. Pass a reference to this type as the param. Returns an array of matching elements.
* @returns {Shape[]} - Empty array if nothing is found.
*/
getByType(type: Shape): Shape[];
/**
* @name Two.Group#add
* @function
* @param {Element[]} objects - An array of objects to be added. Can also be supplied as individual arguments.
* @params {...Element} [args] - Alternatively pass shapes as each argument
* @description Add objects to the group.
*/
add(objects: Shape | Shape[]): Group;
add(...args: Shape[]): Group;
/**
* @name Two.Group#remove
* @function
* @description Remove self from the scene / parent.
*/
remove(): Shape;
/**
* @name Two.Group#remove
* @function
* @param {Shape[]} objects - An array of objects to be removed. Can also be supplied as individual arguments.
* @description Remove objects from the group.
*/
remove(objects: Shape[]): Shape[];
/**
* @name Two.Group#remove
* @function
* @params {...Shape} [args] - Alternatively pass shapes as each argument
* @description Remove objects from the group.
*/
remove(...args: Shape[]): Shape | Shape[];
/**
* @name Two.Group#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the group.
*/
getBoundingClientRect(shallow?: boolean): BoundingBox;
/**
* @name Two.Group#noFill
* @function
* @description Apply `noFill` method to all child shapes.
*/
noFill(): Group;
/**
* @name Two.Group#noStroke
* @function
* @description Apply `noStroke` method to all child shapes.
*/
noStroke(): Group;
/**
* @name Two.Group#subdivide
* @function
* @description Apply `subdivide` method to all child shapes.
*/
subdivide(limit?: number): Group;
/**
* @name Two.Group#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Group}
* @description Create a new instance of {@link Two.Group} with the same properties of the current group.
*/
clone(parent?: Group): Group;
/**
* @name Two.Group#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
protected _update(bubbles?: boolean): Group;
/**
* @name Two.Group#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset(): Group;
}
import { Shape } from 'two.js/src/shape';
import { Path, CapProperties, JoinProperties } from 'two.js/src/path';
import { Text } from 'two.js/src/text';
import { Points } from 'two.js/src/shapes/points';
import { Children } from 'two.js/src/children';
import { Gradient } from 'two.js/src/effects/gradient';
import { Texture } from 'two.js/src/effects/texture';
import { BoundingBox, SceneHitTestOptions } from 'two.js';
}
================================================
FILE: src/group.js
================================================
import { Events } from './events.js';
import { _ } from './utils/underscore.js';
import { getEffectFromObject } from './utils/shape.js';
import { boundsContains } from './utils/hit-test.js';
import { Shape } from './shape.js';
import { Children } from './children.js';
import { Path } from './path.js';
import { ArcSegment } from './shapes/arc-segment.js';
import { Circle } from './shapes/circle.js';
import { Ellipse } from './shapes/ellipse.js';
import { Points } from './shapes/points.js';
import { Polygon } from './shapes/polygon.js';
import { Rectangle } from './shapes/rectangle.js';
import { RoundedRectangle } from './shapes/rounded-rectangle.js';
import { Star } from './shapes/star.js';
import { Text } from './text.js';
import { Element } from './element.js';
import { ImageSequence } from './effects/image-sequence.js';
import { Sprite } from './effects/sprite.js';
// Constants
const min = Math.min,
max = Math.max;
const cache = {
getShapesAtPoint: {
results: [],
hitOptions: {},
context: {
x: 0,
y: 0,
visibleOnly: true,
results: null,
},
single: [],
output: [],
empty: [],
},
};
/**
* @name Two.Group
* @class
* @extends Two.Shape
* @param {Two.Shape[]} [children] - A list of objects that inherit {@link Two.Shape}. For instance, the array could be a {@link Two.Path}, {@link Two.Text}, and {@link Two.RoundedRectangle}.
* @description This is the primary class for grouping objects that are then drawn in Two.js. In Illustrator this is a group, in After Effects it would be a Null Object. Whichever the case, the `Two.Group` contains a transformation matrix and commands to style its children, but it by itself doesn't render to the screen.
* @nota-bene The {@link Two#scene} is an instance of `Two.Group`.
*/
export class Group extends Shape {
/**
* @name Two.Group#_flagAdditions
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#additions} needs updating.
*/
_flagAdditions = false;
/**
* @name Two.Group#_flagSubtractions
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#subtractions} needs updating.
*/
_flagSubtractions = false;
/**
* @name Two.Group#_flagOrder
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#order} needs updating.
*/
_flagOrder = false;
/**
* @name Two.Group#_flagVisible
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#visible} needs updating.
*/
/**
* @name Two.Group#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#opacity} needs updating.
*/
_flagOpacity = true;
/**
* @name Two.Group#_flagBeginning
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#beginning} needs updating.
*/
_flagBeginning = false;
/**
* @name Two.Group#_flagEnding
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#ending} needs updating.
*/
_flagEnding = false;
/**
* @name Two.Group#_flagLength
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#length} needs updating.
*/
_flagLength = false;
/**
* @name Two.Group#_flagMask
* @private
* @property {Boolean} - Determines whether the {@link Two.Group#mask} needs updating.
*/
_flagMask = false;
// Underlying Properties
/**
* @name Two.Group#fill
* @property {(String|Two.Gradient|Two.Texture)} - The value of what all child shapes should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
_fill = '#fff';
/**
* @name Two.Group#stroke
* @property {(String|Two.Gradient|Two.Texture)} - The value of what all child shapes should be outlined in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
_stroke = '#000';
/**
* @name Two.Group#linewidth
* @property {Number} - The thickness in pixels of the stroke for all child shapes.
*/
_linewidth = 1.0;
/**
* @name Two.Group#opacity
* @property {Number} - The opaqueness of all child shapes.
* @nota-bene Becomes multiplied by the individual child's opacity property.
*/
_opacity = 1.0;
/**
* @name Two.Group#visible
* @property {Boolean} - Display the path or not.
* @nota-bene For {@link Two.CanvasRenderer} and {@link Two.WebGLRenderer} when set to false all updating is disabled improving performance dramatically with many objects in the scene.
*/
_visible = true;
/**
* @name Two.Group#cap
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeLinecapProperty}
*/
_cap = 'round';
/**
* @name Two.Group#join
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeLinejoinProperty}
*/
_join = 'round';
/**
* @name Two.Group#miter
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeMiterlimitProperty}
*/
_miter = 4;
/**
* @name Two.Group#closed
* @property {Boolean} - Determines whether a final line is drawn between the final point in the `vertices` array and the first point of all child shapes.
*/
_closed = true;
/**
* @name Two.Group#curved
* @property {Boolean} - When the child's path is `automatic = true` this boolean determines whether the lines between the points are curved or not.
*/
_curved = false;
/**
* @name Two.Group#automatic
* @property {Boolean} - Determines whether or not Two.js should calculate curves, lines, and commands automatically for you or to let the developer manipulate them for themselves.
*/
_automatic = true;
/**
* @name Two.Group#beginning
* @property {Number} - Number between zero and one to state the beginning of where the path is rendered.
* @description {@link Two.Group#beginning} is a percentage value that represents at what percentage into all child shapes should the renderer start drawing.
* @nota-bene This is great for animating in and out stroked paths in conjunction with {@link Two.Group#ending}.
*/
_beginning = 0;
/**
* @name Two.Group#ending
* @property {Number} - Number between zero and one to state the ending of where the path is rendered.
* @description {@link Two.Group#ending} is a percentage value that represents at what percentage into all child shapes should the renderer start drawing.
* @nota-bene This is great for animating in and out stroked paths in conjunction with {@link Two.Group#beginning}.
*/
_ending = 1.0;
/**
* @name Two.Group#length
* @property {Number} - The sum of distances between all child lengths.
*/
_length = 0;
/**
* @name Two.Group#mask
* @property {Two.Shape} - The Two.js object to clip from a group's rendering.
*/
_mask = null;
/**
* @name Two.Group#_strokeAttenuation
* @private
* @see {@link Two.Group#strokeAttenuation}
*/
_strokeAttenuation = true;
constructor(children) {
super();
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
//
this._renderer.type = 'group';
/**
* @name Two.Group#additions
* @property {Two.Shape[]}
* @description An automatically updated list of children that need to be appended to the renderer's scenegraph.
*/
this.additions = [];
/**
* @name Two.Group#subtractions
* @property {Two.Shape[]}
* @description An automatically updated list of children that need to be removed from the renderer's scenegraph.
*/
this.subtractions = [];
/**
* @name Two.Group#children
* @property {Two.Group.Children}
* @description A list of all the children in the scenegraph.
* @nota-bene Ther order of this list indicates the order each element is rendered to the screen.
*/
this.children = Array.isArray(children)
? children
: Array.prototype.slice.call(arguments);
}
static Children = Children;
/**
* @name Two.Group.InsertChildren
* @function
* @param {Two.Shape[]} children - The objects to be inserted.
* @description Cached method to let renderers know children have been added to a {@link Two.Group}.
*/
static InsertChildren(children) {
for (let i = 0; i < children.length; i++) {
replaceParent.call(this, children[i], this);
}
}
/**
* @name Two.Group.RemoveChildren
* @function
* @param {Two.Shape[]} children - The objects to be removed.
* @description Cached method to let renderers know children have been removed from a {@link Two.Group}.
*/
static RemoveChildren(children) {
for (let i = 0; i < children.length; i++) {
replaceParent.call(this, children[i]);
}
}
/**
* @name Two.Group.OrderChildren
* @function
* @description Cached method to let renderers know order has been updated on a {@link Two.Group}.
*/
static OrderChildren(children) {
this._flagOrder = true;
}
/**
* @name Two.Group.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Group}.
*/
static Properties = [
'fill',
'stroke',
'linewidth',
'cap',
'join',
'miter',
'closed',
'curved',
'automatic',
];
static fromObject(obj) {
const group = new Group();
for (let i = 0; i < Group.Properties.length; i++) {
const k = Group.Properties[i];
if (k in obj) {
if (/(fill|stroke)/i.test(k)) {
group[k] =
typeof obj[k] === 'string' ? obj[k] : getEffectFromObject(obj[k]);
} else {
group[k] = obj[k];
}
}
}
if ('mask' in obj) {
group.mask = getShapeFromObject(obj.mask);
}
if ('id' in obj) {
group.id = obj.id;
}
group.children = obj.children.map(getShapeFromObject);
return group;
function getShapeFromObject(child) {
// All of the types of children Two.Group supports
if (child && child.renderer) {
switch (child.renderer.type) {
case 'arc-segment':
return ArcSegment.fromObject(child);
case 'circle':
return Circle.fromObject(child);
case 'element':
return Element.fromObject(child);
case 'ellipse':
return Ellipse.fromObject(child);
case 'group':
return Group.fromObject(child);
case 'image':
return Image.fromObject(child);
case 'image-sequence':
return ImageSequence.fromObject(child);
case 'path':
return Path.fromObject(child);
case 'points':
return Points.fromObject(child);
case 'polygon':
return Polygon.fromObject(child);
case 'rectangle':
return Rectangle.fromObject(child);
case 'rounded-rectangle':
return RoundedRectangle.fromObject(child);
case 'shape':
return Shape.fromObject(child);
case 'sprite':
return Sprite.fromObject(child);
case 'star':
return Star.fromObject(child);
case 'text':
return Text.fromObject(child);
}
}
// Commonly null for empty set
// properties like fill and stroke
return child;
}
}
static IsVisible(element, visibleOnly) {
if (!visibleOnly) {
return true;
}
let current = element;
while (current) {
if (typeof current.visible === 'boolean' && !current.visible) {
return false;
}
if (typeof current.opacity === 'number' && current.opacity <= 0) {
return false;
}
current = current.parent;
}
return true;
}
static VisitForHitTest(
group,
context,
includeGroups,
filter,
hitOptions,
tolerance,
stopOnFirst
) {
const children = group && group.children;
if (!children) {
return false;
}
const results = context.results;
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (!child) {
continue;
}
if (!Group.IsVisible(child, context.visibleOnly)) {
continue;
}
const rect =
typeof child.getBoundingClientRect === 'function'
? child.getBoundingClientRect()
: null;
if (rect && !boundsContains(rect, context.x, context.y, tolerance)) {
continue;
}
if (child instanceof Group) {
if (
includeGroups &&
(!filter || filter(child)) &&
typeof child.contains === 'function' &&
child.contains(context.x, context.y, hitOptions)
) {
results.push(child);
if (stopOnFirst) {
return true;
}
}
if (
Group.VisitForHitTest(
child,
context,
includeGroups,
filter,
hitOptions,
tolerance,
stopOnFirst
)
) {
return true;
}
continue;
}
if (!(child instanceof Shape)) {
continue;
}
if (filter && !filter(child)) {
continue;
}
if (typeof child.contains !== 'function') {
continue;
}
if (child.contains(context.x, context.y, hitOptions)) {
results.push(child);
if (stopOnFirst) {
return true;
}
}
}
return false;
}
/**
* @name Two.Group#copy
* @function
* @param {Two.Group} [group] - The reference {@link Two.Group}
* @returns {Two.Group}
* @description Copy the properties of one {@link Two.Group} onto another.
*/
copy(group) {
super.copy.call(this, group);
console.warn(
'Two.js: attempting to copy group. Two.Group.children copying not supported.'
);
for (let i = 0; i < Group.Properties.length; i++) {
const k = Group.Properties[i];
if (k in group) {
this[k] = group[k];
}
}
return this;
}
/**
* @name Two.Group#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Group}
* @description Create a new instance of {@link Two.Group} with the same properties of the current group.
*/
clone(parent) {
// /**
// * TODO: Group has a gotcha in that it's at the moment required to be bound to
// * an instance of two in order to add elements correctly. This needs to
// * be rethought and fixed.
// */
const clone = new Group();
const children = this.children.map(function (child) {
return child.clone();
});
clone.add(children);
clone.opacity = this.opacity;
if (this.mask) {
clone.mask = this.mask;
}
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.className = this.className;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
if (parent) {
parent.add(clone);
}
return clone._update();
}
/**
* @name Two.Group#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the group.
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = 'group';
result.children = [];
result.opacity = this.opacity;
result.className = this.className;
result.mask = this.mask ? this.mask.toObject() : null;
_.each(
this.children,
(child, i) => {
result.children[i] = child.toObject();
},
this
);
return result;
}
/**
* @name Two.Group#dispose
* @function
* @returns {Two.Group}
* @description Release the group's renderer resources and detach all events.
* This method recursively disposes all child objects, unbinds the children
* collection events, and preserves the renderer type for potential re-attachment
* to a new renderer.
*/
dispose() {
// Call parent dispose to preserve renderer type and unbind events
super.dispose();
// Recursively dispose all children
if (this.children) {
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (typeof child.dispose === 'function') {
child.dispose();
}
}
}
// Unbind children collection events
if (this.children && typeof this.children.unbind === 'function') {
try {
this.children.unbind();
} catch (e) {
// Ignore unbind errors for incomplete Collection objects
}
}
return this;
}
/**
* @name Two.Group#getShapesAtPoint
* @function
* @param {Number} x - X coordinate in world space.
* @param {Number} y - Y coordinate in world space.
* @param {Object} [options] - Hit test configuration.
* @param {Boolean} [options.visibleOnly=true] - Limit results to visible shapes.
* @param {Boolean} [options.includeGroups=false] - Include groups in the hit results.
* @param {('all'|'deepest')} [options.mode='all'] - Whether to return all intersecting shapes or only the top-most.
* @param {Boolean} [options.deepest] - Alias for `mode: 'deepest'`.
* @param {Number} [options.precision] - Segmentation precision for curved geometry.
* @param {Number} [options.tolerance=0] - Pixel tolerance applied to hit testing.
* @param {Boolean} [options.fill] - Override fill testing behaviour.
* @param {Boolean} [options.stroke] - Override stroke testing behaviour.
* @param {Function} [options.filter] - Predicate to filter shapes from the result set.
* @returns {Shape[]} Ordered list of intersecting shapes, front to back.
* @description Traverse the group hierarchy and return shapes that contain the specified point.
* @nota-bene Expects *world-space coordinates* – the same pixel-space you get from the renderer (e.g., mouse `clientX`/`clientY` adjusted for the canvas’s offset and pixel ratio).
*/
getShapesAtPoint(x, y, options) {
const opts = options || {};
const { results, hitOptions, context, single, empty } =
cache.getShapesAtPoint;
results.length = 0;
const mode = opts.mode === 'deepest' || opts.deepest ? 'deepest' : 'all';
const visibleOnly = opts.visibleOnly !== false;
const includeGroups = !!opts.includeGroups;
const filter = typeof opts.filter === 'function' ? opts.filter : null;
const tolerance = typeof opts.tolerance === 'number' ? opts.tolerance : 0;
if (typeof opts.precision === 'number') {
hitOptions.precision = opts.precision;
} else {
delete hitOptions.precision;
}
if (typeof opts.fill !== 'undefined') {
hitOptions.fill = opts.fill;
} else {
delete hitOptions.fill;
}
if (typeof opts.stroke !== 'undefined') {
hitOptions.stroke = opts.stroke;
} else {
delete hitOptions.stroke;
}
hitOptions.tolerance = tolerance;
hitOptions.ignoreVisibility = !visibleOnly;
const stopOnFirst = mode === 'deepest';
context.x = x;
context.y = y;
context.visibleOnly = visibleOnly;
context.results = results;
Group.VisitForHitTest(
this,
context,
includeGroups,
filter,
hitOptions,
tolerance,
stopOnFirst
);
if (stopOnFirst) {
if (results.length > 0) {
const first = results[0];
results.length = 0;
single[0] = first;
single.length = 1;
return single;
}
empty.length = 0;
return empty;
}
const hits = results.slice();
results.length = 0;
return hits;
}
/**
* @name Two.Group#corner
* @function
* @description Orient the children of the group to the upper left-hand corner of that group.
*/
corner() {
const rect = this.getBoundingClientRect(true);
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.translation.x -= rect.left;
child.translation.y -= rect.top;
}
if (this.mask) {
this.mask.translation.x -= rect.left;
this.mask.translation.y -= rect.top;
}
return this;
}
/**
* @name Two.Group#center
* @function
* @description Orient the children of the group to the center of that group.
*/
center() {
const rect = this.getBoundingClientRect(true);
const cx = rect.left + rect.width / 2 - this.translation.x;
const cy = rect.top + rect.height / 2 - this.translation.y;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (child.isShape) {
child.translation.x -= cx;
child.translation.y -= cy;
}
}
if (this.mask) {
this.mask.translation.x -= cx;
this.mask.translation.y -= cy;
}
return this;
}
/**
* @name Two.Group#getById
* @function
* @description Recursively search for id. Returns the first element found.
* @returns {Two.Shape} - Or `null` if nothing is found.
*/
getById(id) {
let found = null;
function search(node) {
if (node.id === id) {
return node;
} else if (node.children) {
if (node.children.ids[id]) {
return node.children.ids[id];
}
for (let i = 0; i < node.children.length; i++) {
found = search(node.children[i]);
if (found) {
return found;
}
}
}
return null;
}
return search(this);
}
/**
* @name Two.Group#getByClassName
* @function
* @description Recursively search for classes. Returns an array of matching elements.
* @returns {Two.Shape[]} - Or empty array if nothing is found.
*/
getByClassName(className) {
const found = [];
function search(node) {
if (Array.prototype.indexOf.call(node.classList, className) >= 0) {
found.push(node);
}
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
search(child);
}
}
return found;
}
return search(this);
}
/**
* @name Two.Group#getByType
* @function
* @description Recursively search for children of a specific type, e.g. {@link Two.Path}. Pass a reference to this type as the param. Returns an array of matching elements.
* @returns {Two.Shape[]} - Empty array if nothing is found.
*/
getByType(type) {
const found = [];
function search(node) {
if (node instanceof type) {
found.push(node);
}
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
search(child);
}
}
return found;
}
return search(this);
}
/**
* @name Two.Group#add
* @function
* @param {Two.Shape[]|...Two.Shape} objects - An array of objects to be added. Can also be supplied as individual arguments.
* @description Add objects to the group.
*/
add(objects) {
// Allow to pass multiple objects either as array or as multiple arguments
// If it's an array also create copy of it in case we're getting passed
// a childrens array directly.
if (!(objects instanceof Array)) {
objects = Array.prototype.slice.call(arguments);
} else {
objects = objects.slice();
}
// Add the objects
for (let i = 0; i < objects.length; i++) {
const child = objects[i];
if (!(child && child.id)) {
continue;
}
const index = Array.prototype.indexOf.call(this.children, child);
if (index >= 0) {
this.children.splice(index, 1);
}
this.children.push(child);
}
return this;
}
/**
* @name Two.Group#remove
* @function
* @param {Two.Shape[]|...Two.Shape} [objects=self] - An array of objects to be removed. Can be also removed as individual arguments. If no arguments are passed, then it removes itself from its parent.
* @description Remove objects from the group.
*/
remove(objects) {
const l = arguments.length,
grandparent = this.parent;
// Allow to call remove without arguments
// This will detach the object from its own parent.
if (l <= 0 && grandparent) {
grandparent.remove(this);
return this;
}
// Allow to pass multiple objects either as array or as multiple arguments
// If it's an array also create copy of it in case we're getting passed
// a childrens array directly.
if (!(objects instanceof Array)) {
objects = Array.prototype.slice.call(arguments);
} else {
objects = objects.slice();
}
// Remove the objects
for (let i = 0; i < objects.length; i++) {
const object = objects[i];
if (!object || !this.children.ids[object.id]) {
continue;
}
const index = this.children.indexOf(object);
if (index >= 0) {
this.children.splice(index, 1);
}
}
return this;
}
/**
* @name Two.Group#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the group.
*/
getBoundingClientRect(shallow) {
let rect, matrix, tc, lc, rc, bc;
// TODO: Update this to not __always__ update. Just when it needs to.
this._update(true);
// Variables need to be defined here, because of nested nature of groups.
let left = Infinity,
right = -Infinity,
top = Infinity,
bottom = -Infinity;
const regex = /texture|gradient/i;
matrix = shallow ? this.matrix : this.worldMatrix;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (!child.visible || regex.test(child._renderer.type)) {
continue;
}
rect = child.getBoundingClientRect(shallow);
tc =
typeof rect.top !== 'number' ||
_.isNaN(rect.top) ||
!isFinite(rect.top);
lc =
typeof rect.left !== 'number' ||
_.isNaN(rect.left) ||
!isFinite(rect.left);
rc =
typeof rect.right !== 'number' ||
_.isNaN(rect.right) ||
!isFinite(rect.right);
bc =
typeof rect.bottom !== 'number' ||
_.isNaN(rect.bottom) ||
!isFinite(rect.bottom);
if (tc || lc || rc || bc) {
continue;
}
if (shallow) {
const [ax, ay] = matrix.multiply(rect.left, rect.top);
const [bx, by] = matrix.multiply(rect.right, rect.top);
const [cx, cy] = matrix.multiply(rect.left, rect.bottom);
const [dx, dy] = matrix.multiply(rect.right, rect.bottom);
top = min(ay, by, cy, dy, top);
left = min(ax, bx, cx, dx, left);
right = max(ax, bx, cx, dx, right);
bottom = max(ay, by, cy, dy, bottom);
} else {
top = min(rect.top, top);
left = min(rect.left, left);
right = max(rect.right, right);
bottom = max(rect.bottom, bottom);
}
}
return {
top: top,
left: left,
right: right,
bottom: bottom,
width: right - left,
height: bottom - top,
};
}
/**
* @name Two.Group#noFill
* @function
* @description Apply `noFill` method to all child shapes.
*/
noFill() {
this.children.forEach(function (child) {
child.noFill();
});
return this;
}
/**
* @name Two.Group#noStroke
* @function
* @description Apply `noStroke` method to all child shapes.
*/
noStroke() {
this.children.forEach(function (child) {
child.noStroke();
});
return this;
}
/**
* @name Two.Group#subdivide
* @function
* @description Apply `subdivide` method to all child shapes.
*/
subdivide() {
const args = arguments;
this.children.forEach(function (child) {
child.subdivide.apply(child, args);
});
return this;
}
/**
* @name Two.Group#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
let i, l, child;
if (this._flagBeginning || this._flagEnding) {
const beginning = Math.min(this._beginning, this._ending);
const ending = Math.max(this._beginning, this._ending);
const length = this.length;
let sum = 0;
const bd = beginning * length;
const ed = ending * length;
for (i = 0; i < this.children.length; i++) {
child = this.children[i];
l = child.length;
if (bd > sum + l) {
child.beginning = 1;
child.ending = 1;
} else if (ed < sum) {
child.beginning = 0;
child.ending = 0;
} else if (bd > sum && bd < sum + l) {
child.beginning = (bd - sum) / l;
child.ending = 1;
} else if (ed > sum && ed < sum + l) {
child.beginning = 0;
child.ending = (ed - sum) / l;
} else {
child.beginning = 0;
child.ending = 1;
}
sum += l;
}
}
return super._update.apply(this, arguments);
}
/**
* @name Two.Group#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
if (this._flagAdditions) {
this.additions.length = 0;
this._flagAdditions = false;
}
if (this._flagSubtractions) {
this.subtractions.length = 0;
this._flagSubtractions = false;
}
this._flagOrder =
this._flagMask =
this._flagOpacity =
this._flagBeginning =
this._flagEnding =
false;
super.flagReset.call(this);
return this;
}
}
const proto = {
visible: {
enumerable: true,
get: function () {
return this._visible;
},
set: function (v) {
this._flagVisible = this._visible !== v || this._flagVisible;
this._visible = v;
},
},
opacity: {
enumerable: true,
get: function () {
return this._opacity;
},
set: function (v) {
this._flagOpacity = this._opacity !== v || this._flagOpacity;
this._opacity = v;
},
},
beginning: {
enumerable: true,
get: function () {
return this._beginning;
},
set: function (v) {
this._flagBeginning = this._beginning !== v || this._flagBeginning;
this._beginning = v;
},
},
ending: {
enumerable: true,
get: function () {
return this._ending;
},
set: function (v) {
this._flagEnding = this._ending !== v || this._flagEnding;
this._ending = v;
},
},
length: {
enumerable: true,
get: function () {
if (this._flagLength || this._length <= 0) {
this._length = 0;
if (!this.children) {
return this._length;
}
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
this._length += child.length;
}
}
return this._length;
},
},
fill: {
enumerable: true,
get: function () {
return this._fill;
},
set: function (v) {
this._fill = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.fill = v;
}
},
},
stroke: {
enumerable: true,
get: function () {
return this._stroke;
},
set: function (v) {
this._stroke = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.stroke = v;
}
},
},
linewidth: {
enumerable: true,
get: function () {
return this._linewidth;
},
set: function (v) {
this._linewidth = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.linewidth = v;
}
},
},
join: {
enumerable: true,
get: function () {
return this._join;
},
set: function (v) {
this._join = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.join = v;
}
},
},
miter: {
enumerable: true,
get: function () {
return this._miter;
},
set: function (v) {
this._miter = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.miter = v;
}
},
},
cap: {
enumerable: true,
get: function () {
return this._cap;
},
set: function (v) {
this._cap = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.cap = v;
}
},
},
closed: {
enumerable: true,
get: function () {
return this._closed;
},
set: function (v) {
this._closed = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.closed = v;
}
},
},
curved: {
enumerable: true,
get: function () {
return this._curved;
},
set: function (v) {
this._curved = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.curved = v;
}
},
},
automatic: {
enumerable: true,
get: function () {
return this._automatic;
},
set: function (v) {
this._automatic = v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
child.automatic = v;
}
},
},
children: {
enumerable: true,
get: function () {
return this._children;
},
set: function (children) {
const insertChildren = Group.InsertChildren.bind(this);
const removeChildren = Group.RemoveChildren.bind(this);
const orderChildren = Group.OrderChildren.bind(this);
if (this._children) {
this._children.unbind();
if (this._children.length > 0) {
removeChildren(this._children);
}
}
this._children = new Children(children);
this._children.bind(Events.Types.insert, insertChildren);
this._children.bind(Events.Types.remove, removeChildren);
this._children.bind(Events.Types.order, orderChildren);
if (children.length > 0) {
insertChildren(children);
}
},
},
mask: {
enumerable: true,
get: function () {
return this._mask;
},
set: function (v) {
this._mask = v;
this._flagMask = true;
if (_.isObject(v) && !v.clip) {
v.clip = true;
}
},
},
/**
* @name Two.Group#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space for all child shapes.
* @description When `strokeAttenuation` is `false`, this property is applied to all child shapes, making their stroke widths automatically adjust to compensate for the group's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke widths scale normally with transformations.
*/
strokeAttenuation: {
enumerable: true,
get: function () {
return this._strokeAttenuation;
},
set: function (v) {
this._strokeAttenuation = !!v;
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (child.strokeAttenuation !== undefined) {
child.strokeAttenuation = v;
}
}
},
},
};
// /**
// * Helper function used to sync parent-child relationship within the
// * `Two.Group.children` object.
// *
// * Set the parent of the passed object to another object
// * and updates parent-child relationships
// * Calling with one arguments will simply remove the parenting
// */
function replaceParent(child, newParent) {
const parent = child.parent;
let index;
if (parent === newParent) {
add();
return;
}
if (parent && parent.children.ids[child.id]) {
index = Array.prototype.indexOf.call(parent.children, child);
parent.children.splice(index, 1);
splice();
}
if (newParent) {
add();
return;
}
splice();
if (parent._flagAdditions && parent.additions.length === 0) {
parent._flagAdditions = false;
}
if (parent._flagSubtractions && parent.subtractions.length === 0) {
parent._flagSubtractions = false;
}
delete child.parent;
function add() {
if (newParent.subtractions.length > 0) {
index = Array.prototype.indexOf.call(newParent.subtractions, child);
if (index >= 0) {
newParent.subtractions.splice(index, 1);
}
}
if (newParent.additions.length > 0) {
index = Array.prototype.indexOf.call(newParent.additions, child);
if (index >= 0) {
newParent.additions.splice(index, 1);
}
}
child.parent = newParent;
newParent.additions.push(child);
newParent._flagAdditions = true;
}
function splice() {
index = Array.prototype.indexOf.call(parent.additions, child);
if (index >= 0) {
parent.additions.splice(index, 1);
}
index = Array.prototype.indexOf.call(parent.subtractions, child);
if (index < 0) {
parent.subtractions.push(child);
parent._flagSubtractions = true;
}
}
}
================================================
FILE: src/matrix.d.ts
================================================
declare module 'two.js/src/matrix' {
/**
* @name Two.Matrix
* @class
* @param {Number} [a=1] - The value for element at the first column and first row.
* @param {Number} [b=0] - The value for element at the second column and first row.
* @param {Number} [c=0] - The value for element at the third column and first row.
* @param {Number} [d=0] - The value for element at the first column and second row.
* @param {Number} [e=1] - The value for element at the second column and second row.
* @param {Number} [f=0] - The value for element at the third column and second row.
* @param {Number} [g=0] - The value for element at the first column and third row.
* @param {Number} [h=0] - The value for element at the second column and third row.
* @param {Number} [i=1] - The value for element at the third column and third row.
* @description A class to store 3 x 3 transformation matrix information. In addition to storing data `Two.Matrix` has suped up methods for commonplace mathematical operations.
* @nota-bene Order is based on how to construct transformation strings for the browser.
*/
export class Matrix extends Events {
/**
* @name Two.Matrix.Identity
* @property {Number[]} - A stored reference to the default value of a 3 x 3 matrix.
*/
static Identity: [1, 0, 0, 0, 1, 0, 0, 0, 1];
/**
* @name Two.Matrix.Multiply
* @function
* @param {Matrix} A
* @param {Matrix} B
* @param {Matrix} [C] - An optional matrix to apply the multiplication to.
* @returns {Matrix} - If an optional `C` matrix isn't passed then a new one is created and returned.
* @description Multiply two matrices together and return the result.
*/
static Multiply(A: Matrix, B: Matrix, C?: Matrix): Matrix;
/**
* @name Two.Matrix.fromObject
* @function
* @param {Object} obj - The object notation of a Two.Matrix to create a new instance
* @returns {Two.Matrix}
* @description Create a new {@link Two.Matrix} from an object notation of a {@link Two.Matrix}.
* @nota-bene Works in conjunction with {@link Two.Matrix#toObject}
*/
static fromObject(
obj:
| object
| {
elements?: number[];
manual?: boolean;
}
): Matrix;
constructor(elements: number[]);
constructor(
a?: number,
b?: number,
c?: number,
d?: number,
e?: number,
f?: number
);
/**
* @name Two.Matrix#elements
* @property {Number[]} - The underlying data stored as an array.
*/
elements: [
number,
number,
number,
number,
number,
number,
number,
number,
number
];
/**
* @name Two.Matrix#manual
* @property {Boolean} - Determines whether Two.js automatically calculates the values for the matrix or if the developer intends to manage the matrix.
* @nota-bene - Setting to `true` nullifies {@link Two.Shape#translation}, {@link Two.Shape#rotation}, and {@link Two.Shape#scale}.
*/
manual: boolean;
/**
* @name Two.Matrix#set
* @function
* @param {Number} a - The value for element at the first column and first row.
* @param {Number} b - The value for element at the second column and first row.
* @param {Number} c - The value for element at the third column and first row.
* @param {Number} d - The value for element at the first column and second row.
* @param {Number} e - The value for element at the second column and second row.
* @param {Number} f - The value for element at the third column and second row.
* @param {Number} g - The value for element at the first column and third row.
* @param {Number} h - The value for element at the second column and third row.
* @param {Number} i - The value for element at the third column and third row.
* @description Set an array of values onto the matrix. Order described in {@link Two.Matrix}.
*/
/**
* @name Two.Matrix#set
* @function
* @param {Number[]} a - The array of elements to apply.
* @description Set an array of values onto the matrix. Order described in {@link Two.Matrix}.
*/
set(
a: number[],
b: any,
c: any,
d: any,
e: any,
f: any,
g: any,
h: any,
i: any
): any;
/**
* @name Two.Matrix#copy
* @function
* @description Copy the matrix of one to the current instance.
*/
copy(m: any): Matrix;
/**
* @name Two.Matrix#identity
* @function
* @description Turn matrix to the identity, like resetting.
*/
identity(): Matrix;
/**
* @name Two.Matrix#multiply
* @function
* @param {Number} a - The scalar to be multiplied.
* @description Multiply all components of the matrix against a single scalar value.
* @overloaded
*/
multiply(a: number): Matrix;
/**
* @name Two.Matrix#multiply
* @function
* @param {Number} a - The x component to be multiplied.
* @param {Number} b - The y component to be multiplied.
* @param {Number} c - The z component to be multiplied.
* @description Multiply all components of a matrix against a 3 component vector.
* @overloaded
*/
multiply(
a: number,
b: number,
c?: number
): [x: number, y: number, z: number];
/**
* @name Two.Matrix#multiply
* @function
* @param {Number} a - The value at the first column and first row of the matrix to be multiplied.
* @param {Number} b - The value at the second column and first row of the matrix to be multiplied.
* @param {Number} c - The value at the third column and first row of the matrix to be multiplied.
* @param {Number} d - The value at the first column and second row of the matrix to be multiplied.
* @param {Number} e - The value at the second column and second row of the matrix to be multiplied.
* @param {Number} f - The value at the third column and second row of the matrix to be multiplied.
* @param {Number} g - The value at the first column and third row of the matrix to be multiplied.
* @param {Number} h - The value at the second column and third row of the matrix to be multiplied.
* @param {Number} i - The value at the third column and third row of the matrix to be multiplied.
* @description Multiply all components of a matrix against another matrix.
* @overloaded
*/
multiply(
a: number,
b: number,
c: number,
d: number,
e: number,
f: number,
g: number,
h: number,
i: number
): Matrix;
/**
* @name Two.Matrix#inverse
* @function
* @param {Matrix} [out] - The optional matrix to apply the inversion to.
* @description Return an inverted version of the matrix. If no optional one is passed a new matrix is created and returned.
*/
inverse(out?: Matrix): Matrix;
/**
* @name Two.Matrix#scale
* @function
* @param {Number} scale - The one dimensional scale to apply to the matrix.
* @description Uniformly scale the transformation matrix.
*/
/**
* @name Two.Matrix#scale
* @function
* @param {Number} sx - The horizontal scale factor.
* @param {Number} sy - The vertical scale factor
* @description Scale the transformation matrix in two dimensions.
*/
scale(sx: number, sy: number, ...args: any[]): Matrix;
/**
* @name Two.Matrix#rotate
* @function
* @param {Number} Number - The amount to rotate in Number.
* @description Rotate the matrix.
*/
rotate(Number: number): Matrix;
/**
* @name Two.Matrix#translate
* @function
* @param {Number} x - The horizontal translation value to apply.
* @param {Number} y - The vertical translation value to apply.
* @description Translate the matrix.
*/
translate(x: number, y: number): Matrix;
/**
* @name Two.Matrix#skewX
* @function
* @param {Number} Number - The amount to skew in Number.
* @description Skew the matrix by an angle in the x axis direction.
*/
skewX(Number: number): Matrix;
/**
* @name Two.Matrix#skewY
* @function
* @param {Number} Number - The amount to skew in Number.
* @description Skew the matrix by an angle in the y axis direction.
*/
skewY(Number: number): Matrix;
/**
* @name Two.Matrix#toString
* @function
* @param {Boolean} [fullMatrix=false] - Return the full 9 elements of the matrix or just 6 for 2D transformations.
* @returns {String} - The transformation matrix as a 6 component string separated by spaces.
* @description Create a transform string. Used for the Two.js rendering APIs.
*/
toString(fullMatrix?: boolean): string;
/**
* @name Two.Matrix#toTransformArray
* @function
* @param {Boolean} [fullMatrix=false] - Return the full 9 elements of the matrix or just 6 in the format for 2D transformations.
* @param {Number[]} [output] - An array empty or otherwise to apply the values to.
* @description Create a transform array. Used for the Two.js rendering APIs.
*/
toTransformArray(fullMatrix?: boolean, output?: number[]): number[];
/**
* @name Two.Matrix#toArray
* @function
* @param {Boolean} [fullMatrix=false] - Return the full 9 elements of the matrix or just 6 for 2D transformations.
* @param {Number[]} [output] - An array empty or otherwise to apply the values to.
* @description Create a transform array. Used for the Two.js rendering APIs.
*/
toArray(fullMatrix?: boolean, output?: number[]): number[];
/**
* @name Two.Matrix#toObject
* @function
* @description Create a JSON compatible object that represents information of the matrix.
*/
toObject(): {
elements: number[];
manual: boolean;
};
/**
* @name Two.Matrix#clone
* @function
* @description Clone the current matrix.
*/
clone(): Matrix;
}
import { Events } from 'two.js/src/events';
}
================================================
FILE: src/matrix.js
================================================
import { NumArray, toFixed, setMatrix } from './utils/math.js';
import { Events } from './events.js';
// Constants
const cos = Math.cos,
sin = Math.sin,
tan = Math.tan;
const array = [];
/**
* @name Two.Matrix
* @class
* @param {Number} [a=1] - The value for element at the first column and first row.
* @param {Number} [b=0] - The value for element at the second column and first row.
* @param {Number} [c=0] - The value for element at the third column and first row.
* @param {Number} [d=0] - The value for element at the first column and second row.
* @param {Number} [e=1] - The value for element at the second column and second row.
* @param {Number} [f=0] - The value for element at the third column and second row.
* @param {Number} [g=0] - The value for element at the first column and third row.
* @param {Number} [h=0] - The value for element at the second column and third row.
* @param {Number} [i=1] - The value for element at the third column and third row.
* @description A class to store 3 x 3 transformation matrix information. In addition to storing data `Two.Matrix` has suped up methods for commonplace mathematical operations.
* @nota-bene Order is based on how to construct transformation strings for the browser.
*/
export class Matrix extends Events {
/**
* @name Two.Matrix#elements
* @property {Number[]} - The underlying data stored as an array.
*/
elements = new NumArray(9);
/**
* @name Two.Matrix#manual
* @property {Boolean} - Determines whether Two.js automatically calculates the values for the matrix or if the developer intends to manage the matrix.
* @nota-bene - Setting to `true` nullifies {@link Two.Shape#translation}, {@link Two.Shape#rotation}, and {@link Two.Shape#scale}.
*/
manual = false;
constructor(a, b, c, d, e, f) {
super();
let elements = a;
if (!Array.isArray(elements)) {
elements = Array.prototype.slice.call(arguments);
}
// initialize the elements with default values.
this.identity();
if (elements.length > 0) {
this.set(elements);
}
}
//
/**
* @name Two.Matrix.Identity
* @property {Number[]} - A stored reference to the default value of a 3 x 3 matrix.
*/
static Identity = [1, 0, 0, 0, 1, 0, 0, 0, 1];
/**
* @name Two.Matrix.Multiply
* @function
* @param {Number[]} A - The first {@link Two.Matrix} to multiply
* @param {Number[]} B - The second {@link Two.Matrix} to multiply
* @param {Number[]} [C] - An optional {@link Two.Matrix} to apply the result to
* @returns {Number[]} - If an optional `C` matrix isn't passed then a new one is created and returned.
* @description Multiply two matrices together and return the result.
*/
static Multiply(A, B, C) {
if (B.length <= 3) {
// Multiply Vector
const e = A;
let x, y, z;
const a = B[0] || 0,
b = B[1] || 0,
c = B[2] || 0;
// Go down rows first
// a, d, g, b, e, h, c, f, i
x = e[0] * a + e[1] * b + e[2] * c;
y = e[3] * a + e[4] * b + e[5] * c;
z = e[6] * a + e[7] * b + e[8] * c;
return [x, y, z];
}
const A0 = A[0],
A1 = A[1],
A2 = A[2];
const A3 = A[3],
A4 = A[4],
A5 = A[5];
const A6 = A[6],
A7 = A[7],
A8 = A[8];
const B0 = B[0],
B1 = B[1],
B2 = B[2];
const B3 = B[3],
B4 = B[4],
B5 = B[5];
const B6 = B[6],
B7 = B[7],
B8 = B[8];
C = C || new NumArray(9);
C[0] = A0 * B0 + A1 * B3 + A2 * B6;
C[1] = A0 * B1 + A1 * B4 + A2 * B7;
C[2] = A0 * B2 + A1 * B5 + A2 * B8;
C[3] = A3 * B0 + A4 * B3 + A5 * B6;
C[4] = A3 * B1 + A4 * B4 + A5 * B7;
C[5] = A3 * B2 + A4 * B5 + A5 * B8;
C[6] = A6 * B0 + A7 * B3 + A8 * B6;
C[7] = A6 * B1 + A7 * B4 + A8 * B7;
C[8] = A6 * B2 + A7 * B5 + A8 * B8;
return C;
}
/**
* @name Two.Matrix.fromObject
* @function
* @param {Object} obj - The object notation of a Two.Matrix to create a new instance
* @returns {Two.Matrix}
* @description Create a new {@link Two.Matrix} from an object notation of a {@link Two.Matrix}.
* @nota-bene Works in conjunction with {@link Two.Matrix#toObject}
*/
static fromObject(obj) {
return new Matrix().copy(obj);
}
/**
* @name Two.Matrix#set
* @function
* @param {Number} a - The value for element at the first column and first row
* @param {Number} b - The value for element at the second column and first row
* @param {Number} c - The value for element at the third column and first row
* @param {Number} d - The value for element at the first column and second row
* @param {Number} e - The value for element at the second column and second row
* @param {Number} f - The value for element at the third column and second row
* @param {Number} g - The value for element at the first column and third row
* @param {Number} h - The value for element at the second column and third row
* @param {Number} i - The value for element at the third column and third row
* @description Set an array of values onto the matrix. Order described in {@link Two.Matrix}.
*/
/**
* @name Two.Matrix#set
* @function
* @param {Number[]} a - The array of elements to apply
* @description Set an array of values onto the matrix. Order described in {@link Two.Matrix}.
*/
set(a, b, c, d, e, f, g, h, i) {
if (typeof b === 'undefined') {
const elements = a;
a = elements[0];
b = elements[1];
c = elements[2];
d = elements[3];
e = elements[4];
f = elements[5];
g = elements[6];
h = elements[7];
i = elements[8];
}
this.elements[0] = a;
this.elements[1] = b;
this.elements[2] = c;
this.elements[3] = d;
this.elements[4] = e;
this.elements[5] = f;
this.elements[6] = g;
this.elements[7] = h;
this.elements[8] = i;
return this.trigger(Events.Types.change);
}
/**
* @name Two.Matrix#copy
* @function
* @param {Two.Matrix} m - The matrix to copy
* @description Copy the matrix of one to the current instance.
*/
copy(m) {
this.elements[0] = m.elements[0];
this.elements[1] = m.elements[1];
this.elements[2] = m.elements[2];
this.elements[3] = m.elements[3];
this.elements[4] = m.elements[4];
this.elements[5] = m.elements[5];
this.elements[6] = m.elements[6];
this.elements[7] = m.elements[7];
this.elements[8] = m.elements[8];
this.manual = m.manual;
return this.trigger(Events.Types.change);
}
/**
* @name Two.Matrix#identity
* @function
* @description Turn matrix to the identity, like resetting.
*/
identity() {
this.elements[0] = Matrix.Identity[0];
this.elements[1] = Matrix.Identity[1];
this.elements[2] = Matrix.Identity[2];
this.elements[3] = Matrix.Identity[3];
this.elements[4] = Matrix.Identity[4];
this.elements[5] = Matrix.Identity[5];
this.elements[6] = Matrix.Identity[6];
this.elements[7] = Matrix.Identity[7];
this.elements[8] = Matrix.Identity[8];
return this.trigger(Events.Types.change);
}
/**
* @name Two.Matrix#multiply
* @function
* @param {Number} s - The scalar to be multiplied.
* @description Multiply all components of the matrix against a single scalar value.
* @overloaded
*/
/**
* @name Two.Matrix#multiply
* @function
* @param {Number} x - The `x` component to be multiplied.
* @param {Number} y - The `y` component to be multiplied.
* @param {Number} z - The `z` component to be multiplied.
* @description Multiply all components of a matrix against a 3 component vector.
* @overloaded
*/
/**
* @name Two.Matrix#multiply
* @function
* @param {Number} a - The value at the first column and first row of the matrix to be multiplied.
* @param {Number} b - The value at the second column and first row of the matrix to be multiplied.
* @param {Number} c - The value at the third column and first row of the matrix to be multiplied.
* @param {Number} d - The value at the first column and second row of the matrix to be multiplied.
* @param {Number} e - The value at the second column and second row of the matrix to be multiplied.
* @param {Number} f - The value at the third column and second row of the matrix to be multiplied.
* @param {Number} g - The value at the first column and third row of the matrix to be multiplied.
* @param {Number} h - The value at the second column and third row of the matrix to be multiplied.
* @param {Number} i - The value at the third column and third row of the matrix to be multiplied.
* @description Multiply all components of a matrix against another matrix.
* @overloaded
*/
multiply(a, b, c, d, e, f, g, h, i) {
// Multiply scalar
if (typeof b === 'undefined') {
this.elements[0] *= a;
this.elements[1] *= a;
this.elements[2] *= a;
this.elements[3] *= a;
this.elements[4] *= a;
this.elements[5] *= a;
this.elements[6] *= a;
this.elements[7] *= a;
this.elements[8] *= a;
return this.trigger(Events.Types.change);
}
if (typeof c === 'undefined') {
c = 1;
}
if (typeof d === 'undefined') {
// Multiply Vector
a = a || 0;
b = b || 0;
c = c || 0;
e = this.elements;
// Go down rows first
// a, d, g, b, e, h, c, f, i
const x = e[0] * a + e[1] * b + e[2] * c;
const y = e[3] * a + e[4] * b + e[5] * c;
const z = e[6] * a + e[7] * b + e[8] * c;
return [x, y, z];
}
// Multiple matrix
const A = this.elements;
const B = [a, b, c, d, e, f, g, h, i];
const A0 = A[0],
A1 = A[1],
A2 = A[2];
const A3 = A[3],
A4 = A[4],
A5 = A[5];
const A6 = A[6],
A7 = A[7],
A8 = A[8];
const B0 = B[0],
B1 = B[1],
B2 = B[2];
const B3 = B[3],
B4 = B[4],
B5 = B[5];
const B6 = B[6],
B7 = B[7],
B8 = B[8];
this.elements[0] = A0 * B0 + A1 * B3 + A2 * B6;
this.elements[1] = A0 * B1 + A1 * B4 + A2 * B7;
this.elements[2] = A0 * B2 + A1 * B5 + A2 * B8;
this.elements[3] = A3 * B0 + A4 * B3 + A5 * B6;
this.elements[4] = A3 * B1 + A4 * B4 + A5 * B7;
this.elements[5] = A3 * B2 + A4 * B5 + A5 * B8;
this.elements[6] = A6 * B0 + A7 * B3 + A8 * B6;
this.elements[7] = A6 * B1 + A7 * B4 + A8 * B7;
this.elements[8] = A6 * B2 + A7 * B5 + A8 * B8;
return this.trigger(Events.Types.change);
}
/**
* @name Two.Matrix#inverse
* @function
* @param {Two.Matrix} [output] - The optional matrix to apply the inversion to.
* @description Return an inverted version of the matrix. If no optional one is passed a new matrix is created and returned.
*/
inverse(output) {
const a = this.elements;
output = output || new Matrix();
const a00 = a[0],
a01 = a[1],
a02 = a[2];
const a10 = a[3],
a11 = a[4],
a12 = a[5];
const a20 = a[6],
a21 = a[7],
a22 = a[8];
const b01 = a22 * a11 - a12 * a21;
const b11 = -a22 * a10 + a12 * a20;
const b21 = a21 * a10 - a11 * a20;
// Calculate the determinant
let det = a00 * b01 + a01 * b11 + a02 * b21;
if (!det) {
return null;
}
det = 1.0 / det;
output.elements[0] = b01 * det;
output.elements[1] = (-a22 * a01 + a02 * a21) * det;
output.elements[2] = (a12 * a01 - a02 * a11) * det;
output.elements[3] = b11 * det;
output.elements[4] = (a22 * a00 - a02 * a20) * det;
output.elements[5] = (-a12 * a00 + a02 * a10) * det;
output.elements[6] = b21 * det;
output.elements[7] = (-a21 * a00 + a01 * a20) * det;
output.elements[8] = (a11 * a00 - a01 * a10) * det;
return output;
}
/**
* @name Two.Matrix#scale
* @function
* @param {Number} s - The one dimensional scale to apply to the matrix.
* @description Uniformly scale the transformation matrix.
*/
/**
* @name Two.Matrix#scale
* @function
* @param {Number} sx - The horizontal scale factor.
* @param {Number} sy - The vertical scale factor
* @description Scale the transformation matrix in two dimensions.
*/
scale(sx, sy) {
const l = arguments.length;
if (l <= 1) {
sy = sx;
}
return this.multiply(sx, 0, 0, 0, sy, 0, 0, 0, 1);
}
/**
* @name Two.Matrix#rotate
* @function
* @param {Number} n - The amount to rotate in Number.
* @description Rotate the matrix.
*/
rotate(n) {
const c = cos(n);
const s = sin(n);
return this.multiply(c, -s, 0, s, c, 0, 0, 0, 1);
}
/**
* @name Two.Matrix#translate
* @function
* @param {Number} x - The horizontal translation value to apply
* @param {Number} y - The vertical translation value to apply
* @description Translate the matrix to specific `x` / `y` values.
*/
translate(x, y) {
return this.multiply(1, 0, x, 0, 1, y, 0, 0, 1);
}
/**
* @name Two.Matrix#skewX
* @function
* @param {Number} n - The amount to skew
* @description Skew the matrix by an angle in the x axis direction.
*/
skewX(n) {
const a = tan(n);
return this.multiply(1, a, 0, 0, 1, 0, 0, 0, 1);
}
/**
* @name Two.Matrix#skewY
* @function
* @param {Number} n - The amount to skew
* @description Skew the matrix by an angle in the y axis direction.
*/
skewY(n) {
const a = tan(n);
return this.multiply(1, 0, 0, a, 1, 0, 0, 0, 1);
}
/**
* @name Two.Matrix#toString
* @function
* @param {Boolean} [fullMatrix=false] - Return the full 9 elements of the matrix or just 6 for 2D transformations.
* @returns {String} - The transformation matrix as a 6 component string separated by spaces.
* @description Create a transform string. Used for the Two.js rendering APIs.
*/
toString(fullMatrix) {
array.length = 0;
this.toTransformArray(fullMatrix, array);
return array.map(toFixed).join(' ');
}
/**
* @name Two.Matrix#toTransformArray
* @function
* @param {Boolean} [fullMatrix=false] - Return the full 9 elements of the matrix or just 6 in the format for 2D transformations.
* @param {Number[]} [output] - An array empty or otherwise to apply the values to.
* @description Create a transform array. Used for the Two.js rendering APIs.
*/
toTransformArray(fullMatrix, output) {
const elements = this.elements;
const hasOutput = !!output;
const a = elements[0];
const b = elements[1];
const c = elements[2];
const d = elements[3];
const e = elements[4];
const f = elements[5];
if (fullMatrix) {
const g = elements[6];
const h = elements[7];
const i = elements[8];
if (hasOutput) {
output[0] = a;
output[1] = d;
output[2] = g;
output[3] = b;
output[4] = e;
output[5] = h;
output[6] = c;
output[7] = f;
output[8] = i;
return;
}
return [a, d, g, b, e, h, c, f, i];
}
if (hasOutput) {
output[0] = a;
output[1] = d;
output[2] = b;
output[3] = e;
output[4] = c;
output[5] = f;
return;
}
return [
a,
d,
b,
e,
c,
f, // Specific format see LN:19
];
}
/**
* @name Two.Matrix#toArray
* @function
* @param {Boolean} [fullMatrix=false] - Return the full 9 elements of the matrix or just 6 for 2D transformations.
* @param {Number[]} [output] - An array empty or otherwise to apply the values to.
* @description Create a transform array. Used for the Two.js rendering APIs.
*/
toArray(fullMatrix, output) {
const elements = this.elements;
const hasOutput = !!output;
const a = elements[0];
const b = elements[1];
const c = elements[2];
const d = elements[3];
const e = elements[4];
const f = elements[5];
if (fullMatrix) {
const g = elements[6];
const h = elements[7];
const i = elements[8];
if (hasOutput) {
output[0] = a;
output[1] = b;
output[2] = c;
output[3] = d;
output[4] = e;
output[5] = f;
output[6] = g;
output[7] = h;
output[8] = i;
return;
}
return [a, b, c, d, e, f, g, h, i];
}
if (hasOutput) {
output[0] = a;
output[1] = b;
output[2] = c;
output[3] = d;
output[4] = e;
output[5] = f;
return;
}
return [a, b, c, d, e, f];
}
/**
* @name Two.Matrix#toObject
* @function
* @description Create a JSON compatible object that represents information of the matrix.
* @nota-bene Works in conjunction with {@link Two.Matrix.fromObject}
*/
toObject() {
return {
renderer: { type: 'matrix' },
elements: this.toArray(true),
manual: !!this.manual,
};
}
/**
* @name Two.Matrix#clone
* @function
* @description Clone the current matrix.
*/
clone() {
return new Matrix().copy(this);
}
}
setMatrix(Matrix);
================================================
FILE: src/path.d.ts
================================================
declare module 'two.js/src/path' {
export type CapProperties = 'butt' | 'round' | 'square';
export type JoinProperties = 'miter' | 'round' | 'bevel';
/**
* @name Two.Path
* @class
* @extends Two.Shape
* @param {Two.Anchor[]} [vertices] - A list of {@link Two.Anchor}s that represent the order and coordinates to construct the rendered shape.
* @param {Boolean} [closed=false] - Describes whether the shape is closed or open.
* @param {Boolean} [curved=false] - Describes whether the shape automatically calculates bezier handles for each vertex.
* @param {Boolean} [manual=false] - Describes whether the developer controls how vertices are plotted or if Two.js automatically plots coordinates based on closed and curved booleans.
* @description This is the primary primitive class for creating all drawable shapes in Two.js. Unless specified methods return their instance of `Two.Path` for the purpose of chaining.
*/
export class Path extends Shape {
/**
* @name Two.Path.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Path}.
*/
static Properties: string[];
static Utils: {
getCurveLength: (
a: Anchor | Vector,
b: Anchor | Vector,
limit: number
) => number;
};
/**
* @name Two.Path.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Path} to create a new instance
* @returns {Two.Path}
* @description Create a new {@link Two.Path} from an object notation of a {@link Two.Path}.
* @nota-bene Works in conjunction with {@link Two.Path#toObject}
*/
static fromObject(
obj: Parameters[0] & {
vertices?: ({ x: number; y: number } | Anchor | Vector)[];
fill?: string;
stroke?: string;
linewidth?: number;
opacity?: number;
visible?: boolean;
cap?: CapProperties;
join?: JoinProperties;
miter?: number;
closed?: boolean;
curved?: boolean;
automatic?: boolean;
beginning?: number;
ending?: number;
dashes?: number[] & {
offset?: number;
};
}
): Path;
constructor(
vertices?: Anchor[],
closed?: boolean,
curved?: boolean,
manual?: boolean
);
/**
* @name Two.Path#_flagVertices
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#vertices} need updating.
*/
private _flagVertices;
/**
* @name Two.Path#_flagLength
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#length} needs updating.
*/
private _flagLength;
/**
* @name Two.Path#_flagFill
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#fill} needs updating.
*/
private _flagFill;
/**
* @name Two.Path#_flagStroke
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#stroke} needs updating.
*/
private _flagStroke;
/**
* @name Two.Path#_flagLinewidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#linewidth} needs updating.
*/
private _flagLinewidth;
/**
* @name Two.Path#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#opacity} needs updating.
*/
private _flagOpacity;
/**
* @name Two.Path#_flagVisible
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#visible} needs updating.
*/
private _flagVisible;
/**
* @name Two.Path#_flagCap
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#cap} needs updating.
*/
private _flagCap;
/**
* @name Two.Path#_flagJoin
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#join} needs updating.
*/
private _flagJoin;
/**
* @name Two.Path#_flagMiter
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#miter} needs updating.
*/
private _flagMiter;
/**
* @name Two.Path#_flagMask
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#mask} needs updating.
*/
private _flagMask;
/**
* @name Two.Path#_flagClip
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#clip} needs updating.
*/
private _flagClip;
/**
* @name Two.Path#_length
* @private
* @see {@link Two.Path#length}
*/
private _length;
/**
* @name Two.Path#_fill
* @private
* @see {@link Two.Path#fill}
*/
private _fill;
/**
* @name Two.Path#_stroke
* @private
* @see {@link Two.Path#stroke}
*/
private _stroke;
/**
* @name Two.Path#_linewidth
* @private
* @see {@link Two.Path#linewidth}
*/
private _linewidth;
/**
* @name Two.Path#_opacity
* @private
* @see {@link Two.Path#opacity}
*/
private _opacity;
/**
* @name Two.Path#_visible
* @private
* @see {@link Two.Path#visible}
*/
private _visible;
/**
* @name Two.Path#_cap
* @private
* @see {@link Two.Path#cap}
*/
private _cap;
/**
* @name Two.Path#_join
* @private
* @see {@link Two.Path#join}
*/
private _join;
/**
* @name Two.Path#_miter
* @private
* @see {@link Two.Path#miter}
*/
private _miter;
/**
* @name Two.Path#_closed
* @private
* @see {@link Two.Path#closed}
*/
private _closed;
/**
* @name Two.Path#_curved
* @private
* @see {@link Two.Path#curved}
*/
private _curved;
/**
* @name Two.Path#_automatic
* @private
* @see {@link Two.Path#automatic}
*/
private _automatic;
/**
* @name Two.Path#_beginning
* @private
* @see {@link Two.Path#beginning}
*/
private _beginning;
/**
* @name Two.Path#_ending
* @private
* @see {@link Two.Path#ending}
*/
private _ending;
/**
* @name Two.Path#_mask
* @private
* @see {@link Two.Path#mask}
*/
private _mask;
/**
* @name Two.Path#_clip
* @private
* @see {@link Two.Path#clip}
*/
private _clip;
/**
* @name Two.Path#_dashes
* @private
* @see {@link Two.Path#dashes}
*/
private _dashes;
/**
* @name Two.Path#_strokeAttenuation
* @private
* @see {@link Two.Path#strokeAttenuation}
*/
private _strokeAttenuation;
/**
* @name Two.Path#closed
* @property {Boolean} - Determines whether a final line is drawn between the final point in the `vertices` array and the first point.
*/
closed: boolean;
/**
* @name Two.Path#curved
* @property {Boolean} - When the path is `automatic = true` this boolean determines whether the lines between the points are curved or not.
*/
curved: boolean;
/**
* @name Two.Path#beginning
* @property {Number} - Number between zero and one to state the beginning of where the path is rendered.
* @description {@link Two.Path#beginning} is a percentage value that represents at what percentage into the path should the renderer start drawing.
* @nota-bene This is great for animating in and out stroked paths in conjunction with {@link Two.Path#ending}.
*/
beginning: number;
/**
* @name Two.Path#ending
* @property {Number} - Number between zero and one to state the ending of where the path is rendered.
* @description {@link Two.Path#ending} is a percentage value that represents at what percentage into the path should the renderer start drawing.
* @nota-bene This is great for animating in and out stroked paths in conjunction with {@link Two.Path#beginning}.
*/
ending: number;
/**
* @name Two.Path#fill
* @property {(String|Gradient|Texture)} - The value of what the path should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
fill: string | Gradient | Texture;
/**
* @name Two.Path#stroke
* @property {(String|Gradient|Texture)} - The value of what the path should be outlined in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
stroke: string | Gradient | Texture;
/**
* @name Two.Path#linewidth
* @property {Number} - The thickness in pixels of the stroke.
*/
linewidth: number;
/**
* @name Two.Path#opacity
* @property {Number} - The opaqueness of the path.
* @nota-bene Can be used in conjunction with CSS Colors that have an alpha value.
*/
opacity: number;
/**
* @name Two.Path#className
* @property {String} - A class to be applied to the element to be compatible with CSS styling.
* @nota-bene Only available for the SVG renderer.
*/
className: string;
/**
* @name Two.Path#visible
* @property {Boolean} - Display the path or not.
* @nota-bene For {@link Two.CanvasRenderer} and {@link Two.WebGLRenderer} when set to false all updating is disabled improving performance dramatically with many objects in the scene.
*/
visible: boolean;
/**
* @name Two.Path#cap
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeLinecapProperty}
*/
cap: CapProperties;
/**
* @name Two.Path#join
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeLinejoinProperty}
*/
join: JoinProperties;
/**
* @name Two.Path#miter
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeMiterlimitProperty}
*/
miter: number;
/**
* @name Two.Path#vertices
* @property {Anchor[]} - An ordered list of anchor points for rendering the path.
* @description A list of {@link Two.Anchor} objects that consist of what form the path takes.
* @nota-bene The array when manipulating is actually a {@link Two.Collection}.
*/
vertices: Anchor[];
/**
* @name Two.Path#automatic
* @property {Boolean} - Determines whether or not Two.js should calculate curves, lines, and commands automatically for you or to let the developer manipulate them for themselves.
*/
automatic: boolean;
/**
* @name Two.Path#dashes
* @type {number[] & { offset?: number }}
* @property {Number[]} - Array of numbers. Odd indices represent dash length. Even indices represent dash space.
* @description A list of numbers that represent the repeated dash length and dash space applied to the stroke of the text.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray} for more information on the SVG stroke-dasharray attribute.
*/
dashes: number[] & {
offset?: number;
};
/**
* @name Two.Path#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
* @description When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
*/
strokeAttenuation: boolean;
/**
* @name Two.Path#copy
* @function
* @param {Two.Path} path - The reference {@link Two.Path}
* @description Copy the properties of one {@link Two.Path} onto another.
*/
copy(path: Path): Path;
/**
* @name Two.Path#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Path}
* @description Create a new instance of {@link Two.Path} with the same properties of the current path.
*/
clone(parent?: Group): Path;
/**
* @name Two.Path#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject(): object;
/**
* @name Two.Path#dispose
* @function
* @description Release the path's renderer resources and detach all events.
* This method cleans up vertices collection events, individual vertex events,
* control point events, and disposes fill/stroke effects (calling dispose()
* on Gradients and Textures for thorough cleanup) while preserving the
* renderer type for potential re-attachment to a new renderer.
*/
dispose(): Path;
/**
* @name Two.Path#noFill
* @function
* @description Short hand method to set fill to `none`.
*/
noFill(): Path;
/**
* @name Two.Path#noStroke
* @function
* @description Short hand method to set stroke to `none`.
*/
noStroke(): Path;
/**
* @name Two.Path#corner
* @function
* @description Orient the vertices of the shape to the upper left-hand corner of the path.
*/
corner(): Path;
/**
* @name Two.Path#center
* @function
* @description Orient the vertices of the shape to the center of the path.
*/
center(): Path;
/**
* @name Two.Path#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the path.
*/
getBoundingClientRect(shallow?: boolean): BoundingBox;
/**
* @name Two.Path#getPointAt
* @function
* @param {Number} t - Percentage value describing where on the {@link Two.Path} to estimate and assign coordinate values.
* @param {Vector} [obj] - Object to apply calculated x, y to. If none available returns new `Object`.
* @returns {Object}
* @description Given a float `t` from 0 to 1, return a point or assign a passed `obj`'s coordinates to that percentage on this {@link Two.Path}'s curve.
*/
getPointAt(t: number, obj?: Vector): Vector;
/**
* @name Two.Path#plot
* @function
* @description Based on closed / curved and sorting of vertices plot where all points should be and where the respective handles should be too.
* @nota-bene While this method is public it is internally called by {@link Two.Path#_update} when `automatic = true`.
*/
plot(): Path;
/**
* @name Two.Path#smooth
* @function
* @param {Object} [options] - Configuration for smoothing.
* @param {String} [options.type='continuous'] - Type of smoothing algorithm.
* @param {Number} [options.from=0] - Index of vertices to start smoothing
* @param {Number} [options.to=1] - Index of vertices to terminate smoothing
* @description Adjust vertex handles to generate smooth curves without toggling `automatic`.
*/
smooth(): Path;
/**
* @name Two.Path#subdivide
* @function
* @param {Number} limit - How many times to recurse subdivisions.
* @description Insert a {@link Two.Anchor} at the midpoint between every item in {@link Two.Path#vertices}.
*/
subdivide(limit: number): Path;
/**
* @name Two.Path#_updateLength
* @function
* @private
* @param {Number} [limit] -
* @param {Boolean} [silent=false] - If set to `true` then the path isn't updated before calculation. Useful for internal use.
* @description Recalculate the {@link Two.Path#length} value.
*/
private _updateLength(limit: number, silent?: boolean): Path;
private _lengths: number[];
/**
* @name Two.Path#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
protected _update(bubbles?: boolean): Path;
/**
* @name Two.Path#contains
* @function
* @param {Number} x - x coordinate to hit test against
* @param {Number} y - y coordinate to hit test against
* @param {Object} [options] - Optional options object
* @param {Boolean} [options.ignoreVisibility] - If `true`, hit test against `path.visible = false` shapes
* @param {Number} [options.tolerance] - Padding to hit test against in pixels
* @returns {Boolean}
* @description Check to see if coordinates are within a {@link Two.Path}'s bounding rectangle
* @nota-bene Expects *world-space coordinates* – the same pixel-space you get from the renderer (e.g., mouse `clientX`/`clientY` adjusted for the canvas’s offset and pixel ratio).
*/
contains(
x: number,
y: number,
options?: { ignoreVisibility: boolean; tolerance: number }
): boolean;
/**
* @name Two.Path#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset(): Path;
}
import { Vector } from 'two.js/src/vector';
import { Anchor } from 'two.js/src/anchor';
import { Shape } from 'two.js/src/shape';
import { Gradient } from 'two.js/src/effects/gradient';
import { Group } from 'two.js/src/group';
import { Texture } from 'two.js/src/effects/texture';
import { BoundingBox } from 'two.js';
/**
* @name FlagVertices
* @private
* @function
* @description Cached method to let renderers know vertices have been updated on a {@link Two.Path}.
*/
export function FlagVertices(): void;
export class FlagVertices {
_flagVertices: boolean;
_flagLength: boolean;
}
/**
* @name BindVertices
* @private
* @function
* @description Cached method to let {@link Two.Path} know vertices have been added to the instance.
*/
export function BindVertices(items: Anchor[]): void;
/**
* @name UnbindVertices
* @private
* @function
* @description Cached method to let {@link Two.Path} know vertices have been removed from the instance.
*/
export function UnbindVertices(items: Anchor[]): void;
/**
* @name FlagFill
* @private
* @function
* @description Cached method to let {@link Two.Path} know the fill has changed.
*/
export function FlagFill(): void;
export class FlagFill {
_flagFill: boolean;
}
/**
* @name FlagFill
* @private
* @function
* @description Cached method to let {@link Two.Path} know the stroke has changed.
*/
export function FlagStroke(): void;
export class FlagStroke {
_flagStroke: boolean;
}
}
================================================
FILE: src/path.js
================================================
import { Commands } from './utils/path-commands.js';
import { Collection } from './collection.js';
import { lerp, mod, decomposeMatrix } from './utils/math.js';
import {
getComponentOnCubicBezier,
getCurveBoundingBox,
getCurveFromPoints,
} from './utils/curves.js';
import {
contains,
getIdByLength,
getCurveLength,
getSubdivisions,
getEffectFromObject,
} from './utils/shape.js';
import { _ } from './utils/underscore.js';
import { Shape } from './shape.js';
import { Events } from './events.js';
import { Vector } from './vector.js';
import { Anchor } from './anchor.js';
import { Matrix } from './matrix.js';
import { Gradient } from './effects/gradient.js';
import { LinearGradient } from './effects/linear-gradient.js';
import { RadialGradient } from './effects/radial-gradient.js';
import { Texture } from './effects/texture.js';
import {
buildPathHitParts,
pointInPolygons,
distanceToSegments,
hasVisibleFill,
hasVisibleStroke,
} from './utils/hit-test.js';
import {
clearHandleComponent,
setHandleComponent,
inheritRelative,
isSegmentCurved,
splitSubdivisionSegment,
applyGlobalSmooth,
applyLocalSmooth,
} from './utils/path.js';
// Constants
const min = Math.min,
max = Math.max,
ceil = Math.ceil,
floor = Math.floor;
const vector = new Vector();
const hitTestMatrix = new Matrix();
/**
* @name Two.Path
* @class
* @extends Two.Shape
* @param {Two.Anchor[]} [vertices] - A list of {@link Two.Anchor}s that represent the order and coordinates to construct the rendered shape.
* @param {Boolean} [closed=false] - Describes whether the shape is closed or open.
* @param {Boolean} [curved=false] - Describes whether the shape automatically calculates bezier handles for each vertex.
* @param {Boolean} [manual=false] - Describes whether the developer controls how vertices are plotted or if Two.js automatically plots coordinates based on closed and curved booleans.
* @description This is the primary primitive class for creating all drawable shapes in Two.js. Unless specified methods return their instance of `Two.Path` for the purpose of chaining.
*/
export class Path extends Shape {
/**
* @name Two.Path#_flagVertices
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#vertices} need updating.
*/
_flagVertices = true;
/**
* @name Two.Path#_flagLength
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#length} needs updating.
*/
_flagLength = true;
/**
* @name Two.Path#_flagFill
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#fill} needs updating.
*/
_flagFill = true;
/**
* @name Two.Path#_flagStroke
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#stroke} needs updating.
*/
_flagStroke = true;
/**
* @name Two.Path#_flagLinewidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#linewidth} needs updating.
*/
_flagLinewidth = true;
/**
* @name Two.Path#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#opacity} needs updating.
*/
_flagOpacity = true;
/**
* @name Two.Path#_flagVisible
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#visible} needs updating.
*/
_flagVisible = true;
/**
* @name Two.Path#_flagCap
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#cap} needs updating.
*/
_flagCap = true;
/**
* @name Two.Path#_flagJoin
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#join} needs updating.
*/
_flagJoin = true;
/**
* @name Two.Path#_flagMiter
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#miter} needs updating.
*/
_flagMiter = true;
/**
* @name Two.Path#_flagStrokeAttenuation
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#strokeAttenuation} needs updating.
*/
_flagStrokeAttenuation = true;
/**
* @name Two.Path#_flagMask
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#mask} needs updating.
*/
_flagMask = false;
/**
* @name Two.Path#_flagClip
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#clip} needs updating.
*/
_flagClip = false;
// Underlying Properties
/**
* @name Two.Path#_length
* @private
* @see {@link Two.Path#length}
*/
_length = 0;
/**
* @name Two.Path#_fill
* @private
* @see {@link Two.Path#fill}
*/
_fill = '#fff';
/**
* @name Two.Path#_stroke
* @private
* @see {@link Two.Path#stroke}
*/
_stroke = '#000';
/**
* @name Two.Path#_linewidth
* @private
* @see {@link Two.Path#linewidth}
*/
_linewidth = 1;
/**
* @name Two.Path#_opacity
* @private
* @see {@link Two.Path#opacity}
*/
_opacity = 1.0;
/**
* @name Two.Path#_visible
* @private
* @see {@link Two.Path#visible}
*/
_visible = true;
/**
* @name Two.Path#_cap
* @private
* @see {@link Two.Path#cap}
*/
_cap = 'round';
/**
* @name Two.Path#_join
* @private
* @see {@link Two.Path#join}
*/
_join = 'round';
/**
* @name Two.Path#_miter
* @private
* @see {@link Two.Path#miter}
*/
_miter = 4;
/**
* @name Two.Path#_closed
* @private
* @see {@link Two.Path#closed}
*/
_closed = true;
/**
* @name Two.Path#_curved
* @private
* @see {@link Two.Path#curved}
*/
_curved = false;
/**
* @name Two.Path#_automatic
* @private
* @see {@link Two.Path#automatic}
*/
_automatic = true;
/**
* @name Two.Path#_beginning
* @private
* @see {@link Two.Path#beginning}
*/
_beginning = 0;
/**
* @name Two.Path#_ending
* @private
* @see {@link Two.Path#ending}
*/
_ending = 1.0;
/**
* @name Two.Path#_mask
* @private
* @see {@link Two.Path#mask}
*/
_mask = null;
/**
* @name Two.Path#_clip
* @private
* @see {@link Two.Path#clip}
*/
_clip = false;
/**
* @name Two.Path#_dashes
* @private
* @see {@link Two.Path#dashes}
*/
_dashes = null;
/**
* @name Two.Path#_strokeAttenuation
* @private
* @see {@link Two.Path#strokeAttenuation}
*/
_strokeAttenuation = true;
constructor(vertices, closed, curved, manual) {
super();
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this._renderer.type = 'path';
this._renderer.flagVertices = FlagVertices.bind(this);
this._renderer.bindVertices = BindVertices.bind(this);
this._renderer.unbindVertices = UnbindVertices.bind(this);
this._renderer.flagFill = FlagFill.bind(this);
this._renderer.flagStroke = FlagStroke.bind(this);
this._renderer.vertices = [];
this._renderer.collection = [];
/**
* @name Two.Path#closed
* @property {Boolean} - Determines whether a final line is drawn between the final point in the `vertices` array and the first point.
*/
this.closed = !!closed;
/**
* @name Two.Path#curved
* @property {Boolean} - When the path is `automatic = true` this boolean determines whether the lines between the points are curved or not.
*/
this.curved = !!curved;
/**
* @name Two.Path#beginning
* @property {Number} - Number between zero and one to state the beginning of where the path is rendered.
* @description {@link Two.Path#beginning} is a percentage value that represents at what percentage into the path should the renderer start drawing.
* @nota-bene This is great for animating in and out stroked paths in conjunction with {@link Two.Path#ending}.
*/
this.beginning = 0;
/**
* @name Two.Path#ending
* @property {Number} - Number between zero and one to state the ending of where the path is rendered.
* @description {@link Two.Path#ending} is a percentage value that represents at what percentage into the path should the renderer start drawing.
* @nota-bene This is great for animating in and out stroked paths in conjunction with {@link Two.Path#beginning}.
*/
this.ending = 1;
// Style properties
/**
* @name Two.Path#fill
* @property {(String|Two.Gradient|Two.Texture)} - The value of what the path should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
this.fill = '#fff';
/**
* @name Two.Path#stroke
* @property {(String|Two.Gradient|Two.Texture)} - The value of what the path should be outlined in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
this.stroke = '#000';
/**
* @name Two.Path#linewidth
* @property {Number} - The thickness in pixels of the stroke.
*/
this.linewidth = 1;
/**
* @name Two.Path#opacity
* @property {Number} - The opaqueness of the path.
* @nota-bene Can be used in conjunction with CSS Colors that have an alpha value.
*/
this.opacity = 1;
/**
* @name Two.Path#className
* @property {String} - A class to be applied to the element to be compatible with CSS styling.
* @nota-bene Only rendered to DOM in the SVG renderer.
*/
this.className = '';
/**
* @name Two.Path#visible
* @property {Boolean} - Display the path or not.
* @nota-bene For {@link Two.CanvasRenderer} and {@link Two.WebGLRenderer} when set to false all updating is disabled improving performance dramatically with many objects in the scene.
*/
this.visible = true;
/**
* @name Two.Path#cap
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeLinecapProperty}
*/
this.cap = 'butt'; // Default of Adobe Illustrator
/**
* @name Two.Path#join
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeLinejoinProperty}
*/
this.join = 'miter'; // Default of Adobe Illustrator
/**
* @name Two.Path#miter
* @property {String}
* @see {@link https://www.w3.org/TR/SVG11/painting.html#StrokeMiterlimitProperty}
*/
this.miter = 4; // Default of Adobe Illustrator
/**
* @name Two.Path#vertices
* @property {Two.Anchor[]} - An ordered list of anchor points for rendering the path.
* @description A list of {@link Two.Anchor} objects that consist of what form the path takes.
* @nota-bene The array when manipulating is actually a {@link Two.Collection}.
*/
this.vertices = vertices;
/**
* @name Two.Path#automatic
* @property {Boolean} - Determines whether or not Two.js should calculate curves, lines, and commands automatically for you or to let the developer manipulate them for themselves.
*/
this.automatic = !manual;
/**
* @name Two.Path#dashes
* @property {Number[]} - Array of numbers. Odd indices represent dash length. Even indices represent dash space.
* @description A list of numbers that represent the repeated dash length and dash space applied to the stroke of the text.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray} for more information on the SVG stroke-dasharray attribute.
*/
this.dashes = [];
/**
* @name Two.Path#dashes#offset
* @property {Number} - A number in pixels to offset {@link Two.Path#dashes} display.
*/
this.dashes.offset = 0;
}
/**
* @name Two.Path.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Path}.
*/
static Properties = [
'fill',
'stroke',
'linewidth',
'opacity',
'visible',
'cap',
'join',
'miter',
'closed',
'curved',
'automatic',
'beginning',
'ending',
'dashes',
'strokeAttenuation',
];
static Utils = {
getCurveLength,
};
/**
* @name Two.Path.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Path} to create a new instance
* @returns {Two.Path}
* @description Create a new {@link Two.Path} from an object notation of a {@link Two.Path}.
* @nota-bene Works in conjunction with {@link Two.Path#toObject}
*/
static fromObject(obj) {
const fill =
typeof obj.fill === 'string' ? obj.fill : getEffectFromObject(obj.fill);
const stroke =
typeof obj.stroke === 'string'
? obj.stroke
: getEffectFromObject(obj.stroke);
const path = new Path().copy({ ...obj, fill, stroke });
if ('id' in obj) {
path.id = obj.id;
}
return path;
}
/**
* @name Two.Path#copy
* @function
* @param {Two.Path} path - The reference {@link Two.Path}
* @description Copy the properties of one {@link Two.Path} onto another.
*/
copy(path) {
super.copy.call(this, path);
if (path.vertices) {
this.vertices = [];
for (let j = 0; j < path.vertices.length; j++) {
const v = path.vertices[j];
if (v instanceof Anchor) {
this.vertices.push(path.vertices[j].clone());
} else {
this.vertices.push(new Anchor().copy(v));
}
}
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
if (k in path) {
this[k] = path[k];
}
}
return this;
}
/**
* @name Two.Path#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Path}
* @description Create a new instance of {@link Two.Path} with the same properties of the current path.
*/
clone(parent) {
const clone = new Path();
for (let j = 0; j < this.vertices.length; j++) {
clone.vertices.push(this.vertices[j].clone());
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
clone.className = this.className;
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
if (parent) {
parent.add(clone);
}
return clone._update();
}
/**
* @name Two.Path#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
* @nota-bene Works in conjunction with {@link Two.Path.fromObject}
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = 'path';
result.vertices = this.vertices.map((v) => v.toObject());
_.each(
Path.Properties,
(k) => {
if (typeof this[k] !== 'undefined') {
if (this[k].toObject) {
result[k] = this[k].toObject();
} else {
result[k] = this[k];
}
}
},
this
);
return result;
}
/**
* @name Two.Path#dispose
* @function
* @returns {Two.Path}
* @description Release the path's renderer resources and detach all events.
* This method cleans up vertices collection events, individual vertex events,
* control point events, and disposes fill/stroke effects (calling dispose()
* on Gradients and Textures for thorough cleanup) while preserving the
* renderer type for potential re-attachment to a new renderer.
*/
dispose() {
// Call parent dispose to preserve renderer type and unbind events
super.dispose();
// Unbind vertices collection events
if (this.vertices && typeof this.vertices.unbind === 'function') {
try {
this.vertices.unbind();
} catch (e) {
// Ignore unbind errors for incomplete Collection objects
}
}
// Unbind individual vertex events and control point events
if (this.vertices) {
for (let i = 0; i < this.vertices.length; i++) {
const v = this.vertices[i];
if (typeof v.unbind === 'function') {
v.unbind();
}
if (v.controls) {
if (v.controls.left && typeof v.controls.left.unbind === 'function') {
v.controls.left.unbind();
}
if (
v.controls.right &&
typeof v.controls.right.unbind === 'function'
) {
v.controls.right.unbind();
}
}
}
}
// Dispose fill effect (more thorough than unbind)
if (
typeof this.fill === 'object' &&
typeof this.fill.dispose === 'function'
) {
this.fill.dispose();
} else if (
typeof this.fill === 'object' &&
typeof this.fill.unbind === 'function'
) {
this.fill.unbind();
}
// Dispose stroke effect (more thorough than unbind)
if (
typeof this.stroke === 'object' &&
typeof this.stroke.dispose === 'function'
) {
this.stroke.dispose();
} else if (
typeof this.stroke === 'object' &&
typeof this.stroke.unbind === 'function'
) {
this.stroke.unbind();
}
return this;
}
/**
* @name Two.Path#noFill
* @function
* @description Short hand method to set fill to `none`.
*/
noFill() {
this.fill = 'none';
return this;
}
/**
* @name Two.Path#noStroke
* @function
* @description Short hand method to set stroke to `none`.
*/
noStroke() {
this.stroke = 'none';
this.linewidth = 0;
return this;
}
/**
* @name Two.Path#corner
* @function
* @description Orient the vertices of the shape to the upper left-hand corner of the path.
*/
corner() {
const rect = this.getBoundingClientRect(true);
const hw = rect.width / 2;
const hh = rect.height / 2;
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
for (let i = 0; i < this.vertices.length; i++) {
const v = this.vertices[i];
v.x -= cx;
v.y -= cy;
v.x += hw;
v.y += hh;
}
if (this.mask) {
this.mask.translation.x -= cx;
this.mask.translation.x += hw;
this.mask.translation.y -= cy;
this.mask.translation.y += hh;
}
return this;
}
/**
* @name Two.Path#center
* @function
* @description Orient the vertices of the shape to the center of the path.
*/
center() {
const rect = this.getBoundingClientRect(true);
const cx = rect.left + rect.width / 2 - this.translation.x;
const cy = rect.top + rect.height / 2 - this.translation.y;
for (let i = 0; i < this.vertices.length; i++) {
const v = this.vertices[i];
v.x -= cx;
v.y -= cy;
}
if (this.mask) {
this.mask.translation.x -= cx;
this.mask.translation.y -= cy;
}
return this;
}
/**
* @name Two.Path#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the path.
*/
getBoundingClientRect(shallow) {
let matrix, border, l, i, v0, v1;
let left = Infinity,
right = -Infinity,
top = Infinity,
bottom = -Infinity;
// TODO: Update this to not __always__ update. Just when it needs to.
this._update(true);
matrix = shallow ? this.matrix : this.worldMatrix;
border = (this.linewidth || 0) / 2;
l = this._renderer.vertices.length;
if (
this.linewidth > 0 ||
(this.stroke && !/(transparent|none)/i.test(this.stroke))
) {
if (this.matrix.manual) {
const { scaleX, scaleY } = decomposeMatrix(
matrix.elements[0],
matrix.elements[3],
matrix.elements[1],
matrix.elements[4],
matrix.elements[2],
matrix.elements[5]
);
if (typeof scaleX === 'number' && typeof scaleY === 'number') {
border = (Math.max(scaleX, scaleY) * (this.linewidth || 0)) / 2;
}
} else {
border *=
typeof this.scale === 'number'
? this.scale
: Math.max(this.scale.x, this.scale.y);
}
}
if (l <= 0) {
return {
width: 0,
height: 0,
};
}
for (i = 0; i < l; i++) {
v1 = this._renderer.vertices[i];
// If i = 0, then this "wraps around" to the last vertex. Otherwise, it's the previous vertex.
// This is important for handling cyclic paths.
v0 = this._renderer.vertices[(i + l - 1) % l];
const [v0x, v0y] = matrix.multiply(v0.x, v0.y);
const [v1x, v1y] = matrix.multiply(v1.x, v1.y);
if (v0.controls && v1.controls) {
let rx = v0.controls.right.x;
let ry = v0.controls.right.y;
if (v0.relative) {
rx += v0.x;
ry += v0.y;
}
let [c0x, c0y] = matrix.multiply(rx, ry);
let lx = v1.controls.left.x;
let ly = v1.controls.left.y;
if (v1.relative) {
lx += v1.x;
ly += v1.y;
}
let [c1x, c1y] = matrix.multiply(lx, ly);
const bb = getCurveBoundingBox(v0x, v0y, c0x, c0y, c1x, c1y, v1x, v1y);
top = min(bb.min.y - border, top);
left = min(bb.min.x - border, left);
right = max(bb.max.x + border, right);
bottom = max(bb.max.y + border, bottom);
} else {
if (i <= 1) {
top = min(v0y - border, top);
left = min(v0x - border, left);
right = max(v0x + border, right);
bottom = max(v0y + border, bottom);
}
top = min(v1y - border, top);
left = min(v1x - border, left);
right = max(v1x + border, right);
bottom = max(v1y + border, bottom);
}
}
return {
top: top,
left: left,
right: right,
bottom: bottom,
width: right - left,
height: bottom - top,
};
}
/**
* @name Two.Path#contains
* @function
* @param {Number} x - x coordinate to hit test against
* @param {Number} y - y coordinate to hit test against
* @param {Object} [options] - Optional options object
* @param {Boolean} [options.ignoreVisibility] - If `true`, hit test against `path.visible = false` shapes
* @param {Number} [options.tolerance] - Padding to hit test against in pixels
* @returns {Boolean}
* @description Check to see if coordinates are within a {@link Two.Path}'s bounding rectangle
* @nota-bene Expects *world-space coordinates* – the same pixel-space you get from the renderer (e.g., mouse `clientX`/`clientY` adjusted for the canvas’s offset and pixel ratio).
*/
contains(x, y, options) {
const opts = options || {};
const ignoreVisibility = opts.ignoreVisibility === true;
if (!ignoreVisibility && this.visible === false) {
return false;
}
if (
!ignoreVisibility &&
typeof this.opacity === 'number' &&
this.opacity <= 0
) {
return false;
}
const tolerance = typeof opts.tolerance === 'number' ? opts.tolerance : 0;
this._update(true);
const rect = this.getBoundingClientRect();
if (
!rect ||
x < rect.left - tolerance ||
x > rect.right + tolerance ||
y < rect.top - tolerance ||
y > rect.bottom + tolerance
) {
return false;
}
const matrix = this.worldMatrix;
const inverse = matrix && matrix.inverse(hitTestMatrix);
if (!inverse) {
return super.contains(x, y, opts);
}
const [localX, localY] = inverse.multiply(x, y, 1);
const precision =
typeof opts.precision === 'number' && !Number.isNaN(opts.precision)
? Math.max(1, Math.floor(opts.precision))
: 8;
const fillTest = hasVisibleFill(this, opts.fill);
const strokeTest = hasVisibleStroke(this, opts.stroke);
const { polygons, segments } = buildPathHitParts(this, precision);
if (fillTest && polygons.length > 0) {
if (pointInPolygons(polygons, localX, localY)) {
return true;
}
}
if (strokeTest && segments.length > 0) {
const linewidth = typeof this.linewidth === 'number' ? this.linewidth : 0;
if (linewidth > 0) {
const distance = distanceToSegments(segments, localX, localY);
if (distance <= linewidth / 2 + tolerance) {
return true;
}
}
}
if (!fillTest && !strokeTest) {
return super.contains(x, y, opts);
}
if (fillTest && polygons.length === 0) {
return super.contains(x, y, opts);
}
return false;
}
/**
* @name Two.Path#getPointAt
* @function
* @param {Number} t - Percentage value describing where on the {@link Two.Path} to estimate and assign coordinate values.
* @param {Two.Vector} [obj] - Object to apply calculated x, y to. If none available returns new `Object`.
* @returns {Object}
* @description Given a float `t` from 0 to 1, return a point or assign a passed `obj`'s coordinates to that percentage on this {@link Two.Path}'s curve.
*/
getPointAt(t, obj) {
let ia, ib, result;
let x, x1, x2, x3, x4, y, y1, y2, y3, y4, left, right;
let target = this.length * Math.min(Math.max(t, 0), 1);
const length = this.vertices.length;
const last = length - 1;
let a = null;
let b = null;
for (let i = 0, l = this._lengths.length, sum = 0; i < l; i++) {
if (sum + this._lengths[i] >= target) {
if (this._closed) {
ia = mod(i, length);
ib = mod(i - 1, length);
if (i === 0) {
ia = ib;
ib = i;
}
} else {
ia = i;
ib = Math.min(Math.max(i - 1, 0), last);
}
a = this.vertices[ia];
b = this.vertices[ib];
target -= sum;
if (this._lengths[i] !== 0) {
t = target / this._lengths[i];
} else {
t = 0;
}
break;
}
sum += this._lengths[i];
}
if (a === null || b === null) {
return null;
}
if (!a) {
return b;
} else if (!b) {
return a;
}
right = b.controls && b.controls.right;
left = a.controls && a.controls.left;
x1 = b.x;
y1 = b.y;
x2 = (right || b).x;
y2 = (right || b).y;
x3 = (left || a).x;
y3 = (left || a).y;
x4 = a.x;
y4 = a.y;
if (right && b.relative) {
x2 += b.x;
y2 += b.y;
}
if (left && a.relative) {
x3 += a.x;
y3 += a.y;
}
x = getComponentOnCubicBezier(t, x1, x2, x3, x4);
y = getComponentOnCubicBezier(t, y1, y2, y3, y4);
// Higher order points for control calculation.
const t1x = lerp(x1, x2, t);
const t1y = lerp(y1, y2, t);
const t2x = lerp(x2, x3, t);
const t2y = lerp(y2, y3, t);
const t3x = lerp(x3, x4, t);
const t3y = lerp(y3, y4, t);
// Calculate the returned points control points.
const brx = lerp(t1x, t2x, t);
const bry = lerp(t1y, t2y, t);
const alx = lerp(t2x, t3x, t);
const aly = lerp(t2y, t3y, t);
if (_.isObject(obj)) {
obj.x = x;
obj.y = y;
if (obj instanceof Anchor) {
obj.controls.left.x = brx;
obj.controls.left.y = bry;
obj.controls.right.x = alx;
obj.controls.right.y = aly;
if (!(typeof obj.relative === 'boolean') || obj.relative) {
obj.controls.left.x -= x;
obj.controls.left.y -= y;
obj.controls.right.x -= x;
obj.controls.right.y -= y;
}
}
obj.t = t;
return obj;
}
result = new Anchor(
x,
y,
brx - x,
bry - y,
alx - x,
aly - y,
this._curved ? Commands.curve : Commands.line
);
result.t = t;
return result;
}
/**
* @name Two.Path#plot
* @function
* @description Based on closed / curved and sorting of vertices plot where all points should be and where the respective handles should be too.
* @nota-bene While this method is public it is internally called by {@link Two.Path#_update} when `automatic = true`.
*/
plot() {
if (this.curved) {
getCurveFromPoints(this._collection, this.closed);
return this;
}
for (let i = 0; i < this._collection.length; i++) {
this._collection[i].command = i === 0 ? Commands.move : Commands.line;
}
return this;
}
/**
* @name Two.Path#smooth
* @function
* @param {Object} [options] - Configuration for smoothing.
* @param {String} [options.type='continuous'] - Type of smoothing algorithm.
* @param {Number} [options.from=0] - Index of vertices to start smoothing
* @param {Number} [options.to=1] - Index of vertices to terminate smoothing
* @description Adjust vertex handles to generate smooth curves without toggling `automatic`.
*/
smooth(options) {
const opts = options || {};
const type = opts.type || 'continuous';
const vertices = this._collection;
const length = vertices.length;
if (length < 2) {
return this;
}
const closed =
this._closed ||
(length > 0 &&
vertices[length - 1] &&
vertices[length - 1].command === Commands.close);
const resolveIndex = (value, defaultIndex) => {
if (value === undefined || value === null) {
return defaultIndex;
}
if (typeof value === 'number') {
if (closed) {
return mod(value, length);
}
let index = value;
if (index < 0) {
index += length;
}
return Math.min(Math.max(index, 0), length - 1);
}
const idx = vertices.indexOf(value);
return idx !== -1 ? idx : defaultIndex;
};
const loop = closed && opts.from === undefined && opts.to === undefined;
let from = resolveIndex(opts.from, 0);
let to = resolveIndex(opts.to, length - 1);
if (from > to) {
if (closed) {
from -= length;
} else {
const temp = from;
from = to;
to = temp;
}
}
const rangeLength = to - from + 1;
for (let i = 0; i < rangeLength; i += 1) {
const index = mod(from + i, length);
const anchor = vertices[index];
const isOpenStart = !closed && index === 0;
if (anchor.command === Commands.move && !isOpenStart) {
anchor.command = Commands.line;
}
}
if (type === 'continuous' || type === 'asymmetric') {
applyGlobalSmooth(
vertices,
from,
to,
closed,
loop,
type === 'asymmetric'
);
} else if (type === 'catmull-rom' || type === 'geometric') {
const range = {
type,
factor: opts.factor,
};
applyLocalSmooth(vertices, from, to, closed, loop, range);
} else {
throw new Error(
`Path.smooth does not support type "${type}". Try 'continuous', 'asymmetric', 'catmull-rom', or 'geometric'.`
);
}
this._automatic = false;
this._flagVertices = true;
this._flagLength = true;
return this;
}
/**
* @name Two.Path#subdivide
* @function
* @param {Number} limit - How many times to recurse subdivisions.
* @description Insert a {@link Two.Anchor} at the midpoint between every item in {@link Two.Path#vertices}.
*/
subdivide(limit) {
this._update();
const vertices = this.vertices;
const length = vertices.length;
if (length < 2) {
return this;
}
const points = [];
let prevOriginal = null;
let subpathStartOriginal = null;
for (let i = 0; i < length; i += 1) {
const currentOriginal = vertices[i];
if (!prevOriginal || currentOriginal.command === Commands.move) {
const clone = currentOriginal.clone();
points.push(clone);
prevOriginal = currentOriginal;
subpathStartOriginal = currentOriginal;
continue;
}
const isCurve = isSegmentCurved(currentOriginal, prevOriginal);
if (isCurve) {
const subdivided = getSubdivisions(
currentOriginal,
prevOriginal,
limit
);
const steps = subdivided.length;
const prevClone = points[points.length - 1];
let startSegment = prevClone.clone();
let endSegment = currentOriginal.clone();
let prevCloneRef = prevClone;
let prevT = 0;
if (steps <= 1) {
const currentClone = currentOriginal.clone();
points.push(currentClone);
} else {
for (let j = 1; j < steps; j += 1) {
const globalT = j / steps;
const denom = 1 - prevT;
const localT =
denom <= Number.EPSILON ? globalT : (globalT - prevT) / denom;
const split = splitSubdivisionSegment(
startSegment,
endSegment,
localT
);
setHandleComponent(
prevCloneRef,
'right',
split.startOut.x - prevCloneRef.x,
split.startOut.y - prevCloneRef.y
);
const newAnchor = split.anchor;
points.push(newAnchor);
prevCloneRef = newAnchor;
startSegment = newAnchor.clone();
prevT = globalT;
setHandleComponent(
endSegment,
'left',
split.endIn.x - endSegment.x,
split.endIn.y - endSegment.y
);
}
const currentClone = currentOriginal.clone();
currentClone.controls.left.copy(endSegment.controls.left);
points.push(currentClone);
}
} else {
const subdivided = getSubdivisions(
currentOriginal,
prevOriginal,
limit
);
for (let j = 1; j < subdivided.length; j += 1) {
const anchor = subdivided[j];
inheritRelative(anchor, prevOriginal);
clearHandleComponent(anchor, 'left');
clearHandleComponent(anchor, 'right');
anchor.command = Commands.line;
points.push(anchor);
}
const currentClone = currentOriginal.clone();
points.push(currentClone);
}
prevOriginal = currentOriginal;
if (currentOriginal.command === Commands.close) {
prevOriginal = subpathStartOriginal;
}
}
this._automatic = false;
this._curved = false;
this.vertices = points;
return this;
}
/**
* @name Two.Path#_updateLength
* @function
* @private
* @param {Number} [limit] -
* @param {Boolean} [silent=false] - If set to `true` then the path isn't updated before calculation. Useful for internal use.
* @description Recalculate the {@link Two.Path#length} value.
*/
_updateLength(limit, silent) {
// TODO: DRYness (function above)
if (!silent) {
this._update();
}
const length = this.vertices.length;
const last = length - 1;
const closed = false; //this._closed || this.vertices[last]._command === Commands.close;
let b = this.vertices[last];
let sum = 0;
if (typeof this._lengths === 'undefined') {
this._lengths = [];
}
_.each(
this.vertices,
function (a, i) {
if ((i <= 0 && !closed) || a.command === Commands.move) {
b = a;
this._lengths[i] = 0;
return;
}
this._lengths[i] = getCurveLength(a, b, limit);
sum += this._lengths[i];
b = a;
},
this
);
this._length = sum;
this._flagLength = false;
return this;
}
/**
* @name Two.Path#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices) {
if (this._automatic) {
this.plot();
}
if (this._flagLength) {
this._updateLength(undefined, true);
}
const l = this._collection.length;
const closed = this._closed;
const beginning = Math.min(this._beginning, this._ending);
const ending = Math.max(this._beginning, this._ending);
const bid = getIdByLength(this, beginning * this._length);
const eid = getIdByLength(this, ending * this._length);
const low = ceil(bid);
const high = floor(eid);
let left, right, prev, next, v, i;
this._renderer.vertices.length = 0;
for (i = 0; i < l; i++) {
if (this._renderer.collection.length <= i) {
// Expected to be `relative` anchor points.
this._renderer.collection.push(new Anchor());
}
if (i > high && !right) {
v = this._renderer.collection[i].copy(this._collection[i]);
this.getPointAt(ending, v);
v.command = this._renderer.collection[i].command;
this._renderer.vertices.push(v);
right = v;
prev = this._collection[i - 1];
// Project control over the percentage `t`
// of the in-between point
if (prev && prev.controls) {
if (v.relative) {
v.controls.right.clear();
} else {
v.controls.right.copy(v);
}
if (prev.relative) {
this._renderer.collection[i - 1].controls.right
.copy(prev.controls.right)
.lerp(Vector.zero, 1 - v.t);
} else {
this._renderer.collection[i - 1].controls.right
.copy(prev.controls.right)
.lerp(prev, 1 - v.t);
}
}
} else if (i >= low && i <= high) {
v = this._renderer.collection[i].copy(this._collection[i]);
this._renderer.vertices.push(v);
if (i === high && contains(this, ending)) {
right = v;
if (!closed && right.controls) {
if (right.relative) {
right.controls.right.clear();
} else {
right.controls.right.copy(right);
}
}
} else if (i === low && contains(this, beginning)) {
left = v;
left.command = Commands.move;
if (!closed && left.controls) {
if (left.relative) {
left.controls.left.clear();
} else {
left.controls.left.copy(left);
}
}
}
}
}
// Prepend the trimmed point if necessary.
if (low > 0 && !left) {
i = low - 1;
v = this._renderer.collection[i].copy(this._collection[i]);
this.getPointAt(beginning, v);
v.command = Commands.move;
this._renderer.vertices.unshift(v);
next = this._collection[i + 1];
// Project control over the percentage `t`
// of the in-between point
if (next && next.controls) {
v.controls.left.clear();
if (next.relative) {
this._renderer.collection[i + 1].controls.left
.copy(next.controls.left)
.lerp(Vector.zero, v.t);
} else {
vector.copy(next);
this._renderer.collection[i + 1].controls.left
.copy(next.controls.left)
.lerp(next, v.t);
}
}
}
}
Shape.prototype._update.apply(this, arguments);
return this;
}
/**
* @name Two.Path#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagVertices =
this._flagLength =
this._flagFill =
this._flagStroke =
this._flagLinewidth =
this._flagOpacity =
this._flagVisible =
this._flagCap =
this._flagJoin =
this._flagMiter =
this._flagClip =
this._flagStrokeAttenuation =
false;
Shape.prototype.flagReset.call(this);
return this;
}
}
const proto = {
linewidth: {
enumerable: true,
get: function () {
return this._linewidth;
},
set: function (v) {
this._linewidth = v;
this._flagLinewidth = true;
},
},
opacity: {
enumerable: true,
get: function () {
return this._opacity;
},
set: function (v) {
this._opacity = v;
this._flagOpacity = true;
},
},
visible: {
enumerable: true,
get: function () {
return this._visible;
},
set: function (v) {
this._visible = v;
this._flagVisible = true;
},
},
cap: {
enumerable: true,
get: function () {
return this._cap;
},
set: function (v) {
this._cap = v;
this._flagCap = true;
},
},
join: {
enumerable: true,
get: function () {
return this._join;
},
set: function (v) {
this._join = v;
this._flagJoin = true;
},
},
miter: {
enumerable: true,
get: function () {
return this._miter;
},
set: function (v) {
this._miter = v;
this._flagMiter = true;
},
},
fill: {
enumerable: true,
get: function () {
return this._fill;
},
set: function (f) {
if (
this._fill instanceof Gradient ||
this._fill instanceof LinearGradient ||
this._fill instanceof RadialGradient ||
this._fill instanceof Texture
) {
this._fill.unbind(Events.Types.change, this._renderer.flagFill);
}
this._fill = f;
this._flagFill = true;
if (
this._fill instanceof Gradient ||
this._fill instanceof LinearGradient ||
this._fill instanceof RadialGradient ||
this._fill instanceof Texture
) {
this._fill.bind(Events.Types.change, this._renderer.flagFill);
}
},
},
stroke: {
enumerable: true,
get: function () {
return this._stroke;
},
set: function (f) {
if (
this._stroke instanceof Gradient ||
this._stroke instanceof LinearGradient ||
this._stroke instanceof RadialGradient ||
this._stroke instanceof Texture
) {
this._stroke.unbind(Events.Types.change, this._renderer.flagStroke);
}
this._stroke = f;
this._flagStroke = true;
if (
this._stroke instanceof Gradient ||
this._stroke instanceof LinearGradient ||
this._stroke instanceof RadialGradient ||
this._stroke instanceof Texture
) {
this._stroke.bind(Events.Types.change, this._renderer.flagStroke);
}
},
},
/**
* @name Two.Path#length
* @property {Number} - The sum of distances between all {@link Two.Path#vertices}.
*/
length: {
get: function () {
if (this._flagLength) {
this._updateLength();
}
return this._length;
},
},
closed: {
enumerable: true,
get: function () {
return this._closed;
},
set: function (v) {
this._closed = !!v;
this._flagVertices = true;
},
},
curved: {
enumerable: true,
get: function () {
return this._curved;
},
set: function (v) {
this._curved = !!v;
this._flagVertices = true;
},
},
automatic: {
enumerable: true,
get: function () {
return this._automatic;
},
set: function (v) {
if (v === this._automatic) {
return;
}
this._automatic = !!v;
const method = this._automatic ? 'ignore' : 'listen';
_.each(this.vertices, function (v) {
v[method]();
});
},
},
beginning: {
enumerable: true,
get: function () {
return this._beginning;
},
set: function (v) {
this._beginning = v;
this._flagVertices = true;
},
},
ending: {
enumerable: true,
get: function () {
return this._ending;
},
set: function (v) {
this._ending = v;
this._flagVertices = true;
},
},
vertices: {
enumerable: true,
get: function () {
return this._collection;
},
set: function (vertices) {
const bindVertices = this._renderer.bindVertices;
const unbindVertices = this._renderer.unbindVertices;
// Remove previous listeners
if (this._collection) {
this._collection
.unbind(Events.Types.insert, bindVertices)
.unbind(Events.Types.remove, unbindVertices);
}
// Create new Collection with copy of vertices
if (vertices instanceof Collection) {
this._collection = vertices;
} else {
this._collection = new Collection(vertices || []);
}
// Listen for Collection changes and bind / unbind
this._collection
.bind(Events.Types.insert, bindVertices)
.bind(Events.Types.remove, unbindVertices);
// Bind Initial Vertices
bindVertices(this._collection);
},
},
/**
* @name Two.Path#mask
* @property {Two.Shape} - The shape whose alpha property becomes a clipping area for the path.
* @nota-bene This property is currently not working because of SVG spec issues found here {@link https://code.google.com/p/chromium/issues/detail?id=370951}.
*/
mask: {
enumerable: true,
get: function () {
return this._mask;
},
set: function (v) {
this._mask = v;
this._flagMask = true;
if (_.isObject(v) && !v.clip) {
v.clip = true;
}
},
},
/**
* @name Two.Path#clip
* @property {Boolean} - Tells Two.js renderer if this object represents a mask for another object (or not).
*/
clip: {
enumerable: true,
get: function () {
return this._clip;
},
set: function (v) {
this._clip = v;
this._flagClip = true;
},
},
dashes: {
enumerable: true,
get: function () {
return this._dashes;
},
set: function (v) {
if (typeof v.offset !== 'number') {
v.offset = (this.dashes && this._dashes.offset) || 0;
}
this._dashes = v;
},
},
/**
* @name Two.Path#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
* @description When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
*/
strokeAttenuation: {
enumerable: true,
get: function () {
return this._strokeAttenuation;
},
set: function (v) {
this._strokeAttenuation = !!v;
this._flagStrokeAttenuation = true;
this._flagLinewidth = true;
},
},
};
// Utility functions
/**
* @name FlagVertices
* @private
* @function
* @description Cached method to let renderers know vertices have been updated on a {@link Two.Path}.
*/
function FlagVertices() {
this._flagVertices = true;
this._flagLength = true;
if (this.parent) {
this.parent._flagLength = true;
}
}
/**
* @name BindVertices
* @private
* @function
* @description Cached method to let {@link Two.Path} know vertices have been added to the instance.
*/
function BindVertices(items) {
// This function is called a lot
// when importing a large SVG
let i = items.length;
while (i--) {
items[i].bind(Events.Types.change, this._renderer.flagVertices);
}
this._renderer.flagVertices();
}
/**
* @name UnbindVertices
* @private
* @function
* @description Cached method to let {@link Two.Path} know vertices have been removed from the instance.
*/
function UnbindVertices(items) {
let i = items.length;
while (i--) {
items[i].unbind(Events.Types.change, this._renderer.flagVertices);
}
this._renderer.flagVertices();
}
/**
* @name FlagFill
* @private
* @function
* @description Cached method to let {@link Two.Path} know the fill has changed.
*/
function FlagFill() {
this._flagFill = true;
}
/**
* @name FlagFill
* @private
* @function
* @description Cached method to let {@link Two.Path} know the stroke has changed.
*/
function FlagStroke() {
this._flagStroke = true;
}
export { FlagVertices, BindVertices, UnbindVertices, FlagFill, FlagStroke };
================================================
FILE: src/registry.d.ts
================================================
declare module 'two.js/src/registry' {
/**
* @name Two.Registry
* @class
* @description An arbitrary class to manage a directory of things. Mainly used for keeping tabs of textures in Two.js.
*/
export class Registry {
map: {};
/**
* @name Two.Registry#add
* @function
* @param {String} id - A unique identifier.
* @param obj - Any type of variable to be registered to the directory.
* @description Adds any value to the directory. Assigned by the `id`.
*/
add(id: string, obj: any): Registry;
/**
* @name Two.Registry#remove
* @function
* @param {String} id - A unique identifier.
* @description Remove any value from the directory by its `id`.
*/
remove(id: string): Registry;
/**
* @name Two.Registry#get
* @function
* @param {String} id - A unique identifier.
* @returns {?Object} The associated value. If unavailable then `undefined` is returned.
* @description Get a registered value by its `id`.
*/
get(id: string): any | null;
}
}
================================================
FILE: src/registry.js
================================================
/**
* @name Two.Registry
* @class
* @description An arbitrary class to manage a directory of things. Mainly used for keeping tabs of textures in Two.js.
*/
export class Registry {
map = {};
constructor() {}
/**
* @name Two.Registry#add
* @function
* @param {String} id - A unique identifier.
* @param obj - Any type of variable to be registered to the directory.
* @description Adds any value to the directory. Assigned by the `id`.
*/
add(id, obj) {
this.map[id] = obj;
return this;
}
/**
* @name Two.Registry#remove
* @function
* @param {String} id - A unique identifier.
* @description Remove any value from the directory by its `id`.
*/
remove(id) {
delete this.map[id];
return this;
}
/**
* @name Two.Registry#get
* @function
* @param {String} id - A unique identifier.
* @returns {?Object} The associated value. If unavailable then `undefined` is returned.
* @description Get a registered value by its `id`.
*/
get(id) {
return this.map[id];
}
/**
* @name Two.Registry#contains
* @function
* @param {String} id - A unique identifier.
* @returns {Boolean}
* @description Convenience method to see if a value is registered to an `id` already.
*/
contains(id) {
return id in this.map;
}
}
================================================
FILE: src/renderers/canvas.d.ts
================================================
declare module 'two.js/src/renderers/canvas' {
/**
* @name Two.CanvasRenderer
* @class
* @param {Object} [parameters] - This object is inherited when constructing a new instance of {@link Two}.
* @param {Element} [parameters.domElement] - The `` to draw to. If none given a new one will be constructed.
* @param {Boolean} [parameters.overdraw] - Determines whether the canvas should clear the background or not. Defaults to `true`.
* @param {Boolean} [parameters.smoothing=true] - Determines whether the canvas should antialias drawing. Set it to `false` when working with pixel art. `false` can lead to better performance, since it would use a cheaper interpolation algorithm.
* @description This class is used by {@link Two} when constructing with `type` of `Two.Types.canvas`. It takes Two.js' scenegraph and renders it to a ``.
*/
export class Renderer extends Events {
/**
* @name Two.CanvasRenderer.Utils
* @property {Object} - A massive object filled with utility functions and properties to render Two.js objects to a ``.
*/
static Utils: {
isHidden: RegExp;
alignments: {
left: string;
middle: string;
right: string;
};
group: {
renderChild: (child: any) => void;
render: (ctx: any) => any;
};
path: {
render: (ctx: any, forced: any, parentClipped: any) => any;
};
points: {
render: (ctx: any, forced: any, parentClipped: any) => any;
};
text: {
render: (ctx: any, forced: any, parentClipped: any) => any;
};
'linear-gradient': {
render: (ctx: any, parent: any) => any;
};
'radial-gradient': {
render: (ctx: any, parent: any) => any;
};
texture: {
render: (ctx: any) => any;
};
renderSvgArcCommand: (
ctx: any,
ax: any,
ay: any,
rx: any,
ry: any,
largeArcFlag: any,
sweepFlag: any,
xAxisRotation: any,
x: any,
y: any
) => void;
};
constructor(params?: any);
/**
* @name Two.CanvasRenderer#domElement
* @property {Element} - The `` associated with the Two.js scene.
*/
domElement: HTMLElement;
/**
* @name Two.CanvasRenderer#ctx
* @property {Canvas2DContext} - Associated two dimensional context to render on the ``.
*/
ctx: CanvasRenderingContext2D;
/**
* @name Two.CanvasRenderer#overdraw
* @property {Boolean} - Determines whether the canvas clears the background each draw call.
* @default true
*/
overdraw: boolean;
/**
* @name Two.CanvasRenderer#scene
* @property {Group} - The root group of the scenegraph.
*/
scene: Group;
/**
* @name Two.CanvasRenderer#setSize
* @function
* @fires resize
* @param {Number} width - The new width of the renderer.
* @param {Number} height - The new height of the renderer.
* @param {Number} [ratio] - The new pixel ratio (pixel density) of the renderer. Defaults to calculate the pixel density of the user's screen.
* @description Change the size of the renderer.
*/
setSize(width: number, height: number, ratio?: number): Renderer;
width: number;
height: number;
ratio: number;
/**
* @name Two.CanvasRenderer#render
* @function
* @description Render the current scene to the ``.
*/
render(): Renderer;
}
import { Events } from 'two.js/src/events';
import { Group } from 'two.js/src/group';
}
================================================
FILE: src/renderers/canvas.js
================================================
import { Commands } from '../utils/path-commands.js';
import {
decomposeMatrix,
mod,
TWO_PI,
getEffectiveStrokeWidth,
} from '../utils/math.js';
import { Curve } from '../utils/curves.js';
import { Events } from '../events.js';
import { getRatio } from '../utils/device-pixel-ratio.js';
import { _ } from '../utils/underscore.js';
import { Group } from '../group.js';
import { Vector } from '../vector.js';
import { Constants } from '../constants.js';
// Constants
const emptyArray = [];
const max = Math.max,
min = Math.min,
abs = Math.abs,
sin = Math.sin,
cos = Math.cos,
acos = Math.acos,
sqrt = Math.sqrt;
const canvas = {
isHidden: /(undefined|none|transparent)/i,
alignments: {
left: 'start',
middle: 'center',
right: 'end',
},
baselines: {
top: 'top',
middle: 'middle',
bottom: 'bottom',
baseline: 'alphabetic',
},
getRendererType: function (type) {
return type in canvas ? type : 'path';
},
group: {
renderChild: function (child) {
const prop = canvas.getRendererType(child._renderer.type);
canvas[prop].render.call(child, this.ctx, true, this.clip);
},
render: function (ctx) {
if (!this._visible) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const matrix = this._matrix.elements;
const parent = this.parent;
this._renderer.opacity =
this._opacity *
(parent && parent._renderer ? parent._renderer.opacity : 1);
const mask = this._mask;
// const clip = this._clip;
const defaultMatrix = isDefaultMatrix(matrix);
const shouldIsolate = !defaultMatrix || !!mask;
if (!this._renderer.context) {
this._renderer.context = {};
}
this._renderer.context.ctx = ctx;
// this._renderer.context.clip = clip;
if (shouldIsolate) {
ctx.save();
if (!defaultMatrix) {
ctx.transform(
matrix[0],
matrix[3],
matrix[1],
matrix[4],
matrix[2],
matrix[5]
);
}
}
if (mask) {
const prop = canvas.getRendererType(mask._renderer.type);
canvas[prop].render.call(mask, ctx, true);
}
if (this._opacity > 0 && this._scale !== 0) {
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
const prop = canvas.getRendererType(child._renderer.type);
canvas[prop].render.call(child, ctx);
}
}
if (shouldIsolate) {
ctx.restore();
}
// Commented two-way functionality of clips / masks with groups and
// polygons. Uncomment when this bug is fixed:
// https://code.google.com/p/chromium/issues/detail?id=370951
// if (clip) {
// ctx.clip();
// }
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
path: {
render: function (ctx, forced, parentClipped) {
let matrix,
stroke,
linewidth,
fill,
opacity,
visible,
cap,
join,
miter,
closed,
commands,
length,
last,
prev,
a,
b,
c,
d,
ux,
uy,
vx,
vy,
ar,
bl,
br,
cl,
x,
y,
mask,
clip,
defaultMatrix,
isOffset,
dashes,
po;
po =
this.parent && this.parent._renderer
? this.parent._renderer.opacity
: 1;
mask = this._mask;
clip = this._clip;
opacity = this._opacity * (po || 1);
visible = this._visible;
if (!forced && (!visible || clip || opacity === 0)) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
matrix = this._matrix.elements;
stroke = this._stroke;
linewidth = this._linewidth;
fill = this._fill;
cap = this._cap;
join = this._join;
miter = this._miter;
closed = this._closed;
commands = this._renderer.vertices; // Commands
length = commands.length;
last = length - 1;
defaultMatrix = isDefaultMatrix(matrix);
dashes = this.dashes;
// Transform
if (!defaultMatrix) {
ctx.save();
ctx.transform(
matrix[0],
matrix[3],
matrix[1],
matrix[4],
matrix[2],
matrix[5]
);
}
// Commented two-way functionality of clips / masks with groups and
// polygons. Uncomment when this bug is fixed:
// https://code.google.com/p/chromium/issues/detail?id=370951
if (mask) {
const prop = canvas.getRendererType(mask._renderer.type);
canvas[prop].render.call(mask, ctx, true);
}
// Styles
if (fill) {
if (typeof fill === 'string') {
ctx.fillStyle = fill;
} else {
const prop = canvas.getRendererType(fill._renderer.type);
canvas[prop].render.call(fill, ctx, this);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === 'string') {
ctx.strokeStyle = stroke;
} else {
const prop = canvas.getRendererType(stroke._renderer.type);
canvas[prop].render.call(stroke, ctx, this);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(this);
}
if (miter) {
ctx.miterLimit = miter;
}
if (join) {
ctx.lineJoin = join;
}
if (!closed && cap) {
ctx.lineCap = cap;
}
}
if (typeof opacity === 'number') {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
ctx.beginPath();
let rx, ry, xAxisRotation, largeArcFlag, sweepFlag, ax, ay;
for (let i = 0; i < length; i++) {
b = commands[i];
x = b.x;
y = b.y;
switch (b.command) {
case Commands.close:
ctx.closePath();
break;
case Commands.arc:
rx = b.rx;
ry = b.ry;
xAxisRotation = b.xAxisRotation;
largeArcFlag = b.largeArcFlag;
sweepFlag = b.sweepFlag;
prev = closed ? mod(i - 1, length) : max(i - 1, 0);
a = commands[prev];
ax = a.x;
ay = a.y;
canvas.renderSvgArcCommand(
ctx,
ax,
ay,
rx,
ry,
largeArcFlag,
sweepFlag,
xAxisRotation,
x,
y
);
break;
case Commands.curve:
prev = closed ? mod(i - 1, length) : Math.max(i - 1, 0);
a = commands[prev];
ar = (a.controls && a.controls.right) || Vector.zero;
bl = (b.controls && b.controls.left) || Vector.zero;
if (a._relative) {
vx = ar.x + a.x;
vy = ar.y + a.y;
} else {
vx = ar.x;
vy = ar.y;
}
if (b._relative) {
ux = bl.x + b.x;
uy = bl.y + b.y;
} else {
ux = bl.x;
uy = bl.y;
}
ctx.bezierCurveTo(vx, vy, ux, uy, x, y);
if (i >= last && closed) {
c = d;
br = (b.controls && b.controls.right) || Vector.zero;
cl = (c.controls && c.controls.left) || Vector.zero;
if (b._relative) {
vx = br.x + b.x;
vy = br.y + b.y;
} else {
vx = br.x;
vy = br.y;
}
if (c._relative) {
ux = cl.x + c.x;
uy = cl.y + c.y;
} else {
ux = cl.x;
uy = cl.y;
}
x = c.x;
y = c.y;
ctx.bezierCurveTo(vx, vy, ux, uy, x, y);
}
break;
case Commands.line:
ctx.lineTo(x, y);
break;
case Commands.move:
d = b;
ctx.moveTo(x, y);
break;
}
}
// Loose ends
if (closed) {
ctx.closePath();
}
if (!clip && !parentClipped) {
if (!canvas.isHidden.test(fill)) {
isOffset = fill._renderer && fill._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(fill._renderer.scale.x, fill._renderer.scale.y);
}
ctx.fill();
if (isOffset) {
ctx.restore();
}
}
if (!canvas.isHidden.test(stroke)) {
isOffset = stroke._renderer && stroke._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(
-stroke._renderer.offset.x,
-stroke._renderer.offset.y
);
ctx.scale(stroke._renderer.scale.x, stroke._renderer.scale.y);
ctx.lineWidth = linewidth / stroke._renderer.scale.x;
}
ctx.stroke();
if (isOffset) {
ctx.restore();
}
}
}
if (!defaultMatrix) {
ctx.restore();
}
if (clip && !parentClipped) {
ctx.clip();
}
if (dashes && dashes.length > 0) {
ctx.setLineDash(emptyArray);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
points: {
render: function (ctx, forced, parentClipped) {
let me,
stroke,
linewidth,
fill,
opacity,
visible,
size,
commands,
length,
b,
x,
y,
defaultMatrix,
isOffset,
dashes,
po;
po =
this.parent && this.parent._renderer
? this.parent._renderer.opacity
: 1;
opacity = this._opacity * (po || 1);
visible = this._visible;
if (!forced && (!visible || opacity === 0)) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
me = this._matrix.elements;
stroke = this._stroke;
linewidth = this._linewidth;
fill = this._fill;
commands = this._renderer.collection; // Commands
length = commands.length;
defaultMatrix = isDefaultMatrix(me);
dashes = this.dashes;
size = this._size;
// Transform
if (!defaultMatrix) {
ctx.save();
ctx.transform(me[0], me[3], me[1], me[4], me[2], me[5]);
}
// Styles
if (fill) {
if (typeof fill === 'string') {
ctx.fillStyle = fill;
} else {
const prop = canvas.getRendererType(fill._renderer.type);
canvas[prop].render.call(fill, ctx, this);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === 'string') {
ctx.strokeStyle = stroke;
} else {
const prop = canvas.getRendererType(stroke._renderer.type);
canvas[prop].render.call(stroke, ctx, this);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(this);
}
}
if (typeof opacity === 'number') {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
ctx.beginPath();
let radius = size * 0.5,
m;
if (!this._sizeAttenuation) {
m = this.worldMatrix.elements;
m = decomposeMatrix(m[0], m[3], m[1], m[4], m[2], m[5]);
radius /= Math.max(m.scaleX, m.scaleY);
}
for (let i = 0; i < length; i++) {
b = commands[i];
x = b.x;
y = b.y;
ctx.moveTo(x + radius, y);
ctx.arc(x, y, radius, 0, TWO_PI);
}
if (!parentClipped) {
if (!canvas.isHidden.test(fill)) {
isOffset = fill._renderer && fill._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(fill._renderer.scale.x, fill._renderer.scale.y);
}
ctx.fill();
if (isOffset) {
ctx.restore();
}
}
if (!canvas.isHidden.test(stroke)) {
isOffset = stroke._renderer && stroke._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(
-stroke._renderer.offset.x,
-stroke._renderer.offset.y
);
ctx.scale(stroke._renderer.scale.x, stroke._renderer.scale.y);
ctx.lineWidth = linewidth / stroke._renderer.scale.x;
}
ctx.stroke();
if (isOffset) {
ctx.restore();
}
}
}
// Loose ends
if (!defaultMatrix) {
ctx.restore();
}
if (dashes && dashes.length > 0) {
ctx.setLineDash(emptyArray);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
text: {
render: function (ctx, forced, parentClipped) {
const po =
this.parent && this.parent._renderer
? this.parent._renderer.opacity
: 1;
const opacity = this._opacity * po;
const visible = this._visible;
const mask = this._mask;
const clip = this._clip;
if (!forced && (!visible || clip || opacity === 0)) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const matrix = this._matrix.elements;
const stroke = this._stroke;
const linewidth = this._linewidth;
const fill = this._fill;
const decoration = this._decoration;
const direction = this._direction;
const defaultMatrix = isDefaultMatrix(matrix);
const isOffset =
fill._renderer &&
fill._renderer.offset &&
stroke._renderer &&
stroke._renderer.offset;
const dashes = this.dashes;
const alignment = canvas.alignments[this._alignment] || this._alignment;
const baseline = canvas.baselines[this._baseline] || this._baseline;
let a, b, c, d, e, sx, sy, x1, y1, x2, y2;
// Transform
if (!defaultMatrix) {
ctx.save();
ctx.transform(
matrix[0],
matrix[3],
matrix[1],
matrix[4],
matrix[2],
matrix[5]
);
}
// Commented two-way functionality of clips / masks with groups and
// polygons. Uncomment when this bug is fixed:
// https://code.google.com/p/chromium/issues/detail?id=370951
if (mask) {
const prop = canvas.getRendererType(mask._renderer.type);
canvas[prop].render.call(mask, ctx, true);
}
if (!isOffset) {
ctx.font = [
this._style,
this._weight,
this._size + 'px/' + this._leading + 'px',
this._family,
].join(' ');
}
ctx.textAlign = alignment;
ctx.textBaseline = baseline;
ctx.direction = direction;
// Styles
if (fill) {
if (typeof fill === 'string') {
ctx.fillStyle = fill;
} else {
const prop = canvas.getRendererType(fill._renderer.type);
canvas[prop].render.call(fill, ctx, this);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === 'string') {
ctx.strokeStyle = stroke;
} else {
const prop = canvas.getRendererType(stroke._renderer.type);
canvas[prop].render.call(stroke, ctx, this);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(this);
}
}
if (typeof opacity === 'number') {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
if (!clip && !parentClipped) {
if (!canvas.isHidden.test(fill)) {
if (fill._renderer && fill._renderer.offset) {
sx = fill._renderer.scale.x;
sy = fill._renderer.scale.y;
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(sx, sy);
a = this._size / fill._renderer.scale.y;
b = this._leading / fill._renderer.scale.y;
ctx.font = [
this._style,
this._weight,
a + 'px/',
b + 'px',
this._family,
].join(' ');
c = fill._renderer.offset.x / fill._renderer.scale.x;
d = fill._renderer.offset.y / fill._renderer.scale.y;
ctx.fillText(this.value, c, d);
ctx.restore();
} else {
ctx.fillText(this.value, 0, 0);
}
}
if (!canvas.isHidden.test(stroke)) {
if (stroke._renderer && stroke._renderer.offset) {
sx = stroke._renderer.scale.x;
sy = stroke._renderer.scale.y;
ctx.save();
ctx.translate(
-stroke._renderer.offset.x,
-stroke._renderer.offset.y
);
ctx.scale(sx, sy);
a = this._size / stroke._renderer.scale.y;
b = this._leading / stroke._renderer.scale.y;
ctx.font = [
this._style,
this._weight,
a + 'px/',
b + 'px',
this._family,
].join(' ');
c = stroke._renderer.offset.x / stroke._renderer.scale.x;
d = stroke._renderer.offset.y / stroke._renderer.scale.y;
e = linewidth / stroke._renderer.scale.x;
ctx.lineWidth = e;
ctx.strokeText(this.value, c, d);
ctx.restore();
} else {
ctx.strokeText(this.value, 0, 0);
}
}
}
// Handle text-decoration
if (/(underline|strikethrough)/i.test(decoration)) {
const metrics = ctx.measureText(this.value);
let scalar = 1;
switch (decoration) {
case 'underline':
y1 = metrics.actualBoundingBoxDescent;
y2 = metrics.actualBoundingBoxDescent;
break;
case 'strikethrough':
y1 = 0;
y2 = 0;
scalar = 0.5;
break;
}
switch (baseline) {
case 'top':
y1 += this._size * scalar;
y2 += this._size * scalar;
break;
case 'baseline':
case 'bottom':
y1 -= this._size * scalar;
y2 -= this._size * scalar;
break;
}
switch (alignment) {
case 'left':
case 'start':
x1 = 0;
x2 = metrics.width;
break;
case 'right':
case 'end':
x1 = -metrics.width;
x2 = 0;
break;
default:
x1 = -metrics.width / 2;
x2 = metrics.width / 2;
}
ctx.lineWidth = Math.max(Math.floor(this._size / 15), 1);
ctx.strokeStyle = ctx.fillStyle;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
if (!defaultMatrix) {
ctx.restore();
}
// TODO: Test for text
if (clip && !parentClipped) {
ctx.clip();
}
if (dashes && dashes.length > 0) {
ctx.setLineDash(emptyArray);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
'linear-gradient': {
render: function (ctx, parent) {
if (!parent) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
if (
!this._renderer.effect ||
this._flagEndPoints ||
this._flagStops ||
this._flagUnits
) {
let rect;
let lx = this.left._x;
let ly = this.left._y;
let rx = this.right._x;
let ry = this.right._y;
if (/objectBoundingBox/i.test(this._units)) {
// Convert objectBoundingBox units to userSpaceOnUse units
rect = parent.getBoundingClientRect(true);
lx = (lx - 0.5) * rect.width;
ly = (ly - 0.5) * rect.height;
rx = (rx - 0.5) * rect.width;
ry = (ry - 0.5) * rect.height;
}
this._renderer.effect = ctx.createLinearGradient(lx, ly, rx, ry);
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
this._renderer.effect.addColorStop(stop._offset, stop._color);
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
'radial-gradient': {
render: function (ctx, parent) {
if (!parent) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
if (
!this._renderer.effect ||
this._flagCenter ||
this._flagFocal ||
this._flagRadius ||
this._flagStops ||
this._flagUnits
) {
let rect;
let cx = this.center._x;
let cy = this.center._y;
let fx = this.focal._x;
let fy = this.focal._y;
let radius = this._radius;
if (/objectBoundingBox/i.test(this._units)) {
// Convert objectBoundingBox units to userSpaceOnUse units
rect = parent.getBoundingClientRect(true);
cx = (cx - 0.5) * rect.width * 0.5;
cy = (cy - 0.5) * rect.height * 0.5;
fx = (fx - 0.5) * rect.width * 0.5;
fy = (fy - 0.5) * rect.height * 0.5;
radius *= Math.min(rect.width, rect.height);
}
this._renderer.effect = ctx.createRadialGradient(
cx,
cy,
0,
fx,
fy,
radius
);
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
this._renderer.effect.addColorStop(stop._offset, stop._color);
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
texture: {
render: function (ctx) {
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const image = this.image;
if (
!this._renderer.effect ||
((this._flagLoaded ||
this._flagImage ||
this._flagVideo ||
this._flagRepeat) &&
this.loaded)
) {
this._renderer.effect = ctx.createPattern(this.image, this._repeat);
}
if (this._flagOffset || this._flagLoaded || this._flagScale) {
if (!(this._renderer.offset instanceof Vector)) {
this._renderer.offset = new Vector();
}
this._renderer.offset.x = -this._offset.x;
this._renderer.offset.y = -this._offset.y;
if (image) {
this._renderer.offset.x += image.width / 2;
this._renderer.offset.y += image.height / 2;
if (this._scale instanceof Vector) {
this._renderer.offset.x *= this._scale.x;
this._renderer.offset.y *= this._scale.y;
} else {
this._renderer.offset.x *= this._scale;
this._renderer.offset.y *= this._scale;
}
}
}
if (this._flagScale || this._flagLoaded) {
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
if (this._scale instanceof Vector) {
this._renderer.scale.copy(this._scale);
} else {
this._renderer.scale.set(this._scale, this._scale);
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
renderSvgArcCommand: function (
ctx,
ax,
ay,
rx,
ry,
largeArcFlag,
sweepFlag,
xAxisRotation,
x,
y
) {
xAxisRotation = (xAxisRotation * Math.PI) / 180;
// Ensure radii are positive
rx = abs(rx);
ry = abs(ry);
// Compute (x1′, y1′)
const dx2 = (ax - x) / 2.0;
const dy2 = (ay - y) / 2.0;
const x1p = cos(xAxisRotation) * dx2 + sin(xAxisRotation) * dy2;
const y1p = -sin(xAxisRotation) * dx2 + cos(xAxisRotation) * dy2;
// Compute (cx′, cy′)
const x1ps = x1p * x1p;
const y1ps = y1p * y1p;
let rxs = rx * rx;
let rys = ry * ry;
// Ensure radii are large enough
const cr = x1ps / rxs + y1ps / rys;
if (cr > 1) {
// scale up rx,ry equally so cr == 1
const s = sqrt(cr);
rx = s * rx;
ry = s * ry;
rxs = rx * rx;
rys = ry * ry;
}
const dq = rxs * y1ps + rys * x1ps;
const pq = (rxs * rys - dq) / dq;
let q = sqrt(max(0, pq));
if (largeArcFlag === sweepFlag) q = -q;
const cxp = (q * rx * y1p) / ry;
const cyp = (-q * ry * x1p) / rx;
// Step 3: Compute (cx, cy) from (cx′, cy′)
const cx =
cos(xAxisRotation) * cxp - sin(xAxisRotation) * cyp + (ax + x) / 2;
const cy =
sin(xAxisRotation) * cxp + cos(xAxisRotation) * cyp + (ay + y) / 2;
// Step 4: Compute θ1 and Δθ
const startAngle = svgAngle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
const delta =
svgAngle(
(x1p - cxp) / rx,
(y1p - cyp) / ry,
(-x1p - cxp) / rx,
(-y1p - cyp) / ry
) % TWO_PI;
const endAngle = startAngle + delta;
const clockwise = sweepFlag === 0;
renderArcEstimate(
ctx,
cx,
cy,
rx,
ry,
startAngle,
endAngle,
clockwise,
xAxisRotation
);
},
};
/**
* @name Two.CanvasRenderer
* @class
* @extends Two.Events
* @param {Object} [parameters] - This object is inherited when constructing a new instance of {@link Two}.
* @param {Element} [parameters.domElement] - The `` to draw to. If none given a new one will be constructed.
* @param {Boolean} [parameters.overdraw] - Determines whether the canvas should clear the background or not. Defaults to `true`.
* @param {Boolean} [parameters.smoothing=true] - Determines whether the canvas should antialias drawing. Set it to `false` when working with pixel art. `false` can lead to better performance, since it would use a cheaper interpolation algorithm.
* @description This class is used by {@link Two} when constructing with `type` of `Two.Types.canvas`. It takes Two.js' scenegraph and renders it to a ``.
*/
export class Renderer extends Events {
constructor(params) {
super();
// It might not make a big difference on GPU backed canvases.
const smoothing = params.smoothing !== false;
/**
* @name Two.CanvasRenderer#domElement
* @property {Element} - The `` associated with the Two.js scene.
*/
this.domElement = params.domElement || document.createElement('canvas');
/**
* @name Two.CanvasRenderer#ctx
* @property {Canvas2DContext} - Associated two dimensional context to render on the ``.
*/
this.ctx = this.domElement.getContext('2d');
/**
* @name Two.CanvasRenderer#overdraw
* @property {Boolean} - Determines whether the canvas clears the background each draw call.
* @default true
*/
this.overdraw = params.overdraw || false;
if (typeof this.ctx.imageSmoothingEnabled !== 'undefined') {
this.ctx.imageSmoothingEnabled = smoothing;
}
/**
* @name Two.CanvasRenderer#scene
* @property {Two.Group} - The root group of the scenegraph.
*/
this.scene = new Group();
this.scene.parent = this;
}
/**
* @name Two.CanvasRenderer.Utils
* @property {Object} - A massive object filled with utility functions and properties to render Two.js objects to a ``.
*/
static Utils = canvas;
/**
* @name Two.CanvasRenderer#setSize
* @function
* @fires resize
* @param {Number} width - The new width of the renderer.
* @param {Number} height - The new height of the renderer.
* @param {Number} [ratio] - The new pixel ratio (pixel density) of the renderer. Defaults to calculate the pixel density of the user's screen.
* @description Change the size of the renderer.
*/
setSize(width, height, ratio) {
this.width = width;
this.height = height;
this.ratio = typeof ratio === 'undefined' ? getRatio(this.ctx) : ratio;
this.domElement.width = width * this.ratio;
this.domElement.height = height * this.ratio;
if (this.domElement.style) {
_.extend(this.domElement.style, {
width: width + 'px',
height: height + 'px',
});
}
return this.trigger(Events.Types.resize, width, height, ratio);
}
/**
* @name Two.CanvasRenderer#render
* @function
* @description Render the current scene to the ``.
*/
render() {
const isOne = this.ratio === 1;
if (!isOne) {
this.ctx.save();
this.ctx.scale(this.ratio, this.ratio);
}
if (!this.overdraw) {
this.ctx.clearRect(0, 0, this.width, this.height);
}
canvas.group.render.call(this.scene, this.ctx);
if (!isOne) {
this.ctx.restore();
}
return this;
}
}
function renderArcEstimate(
ctx,
ox,
oy,
rx,
ry,
startAngle,
endAngle,
clockwise,
xAxisRotation
) {
const delta = endAngle - startAngle;
const epsilon = Curve.Tolerance.epsilon;
const samePoints = Math.abs(delta) < epsilon;
// ensures that deltaAngle is 0 .. 2 PI
let deltaAngle = mod(delta, TWO_PI);
if (deltaAngle < epsilon) {
if (samePoints) {
deltaAngle = 0;
} else {
deltaAngle = TWO_PI;
}
}
if (clockwise === true && !samePoints) {
if (deltaAngle === TWO_PI) {
deltaAngle = -TWO_PI;
} else {
deltaAngle = deltaAngle - TWO_PI;
}
}
for (let i = 0; i < Constants.Resolution; i++) {
const t = i / (Constants.Resolution - 1);
const angle = startAngle + t * deltaAngle;
let x = ox + rx * Math.cos(angle);
let y = oy + ry * Math.sin(angle);
if (xAxisRotation !== 0) {
const cos = Math.cos(xAxisRotation);
const sin = Math.sin(xAxisRotation);
const tx = x - ox;
const ty = y - oy;
// Rotate the point about the center of the ellipse.
x = tx * cos - ty * sin + ox;
y = tx * sin + ty * cos + oy;
}
ctx.lineTo(x, y);
}
}
function svgAngle(ux, uy, vx, vy) {
const dot = ux * vx + uy * vy;
const len = sqrt(ux * ux + uy * uy) * sqrt(vx * vx + vy * vy);
// floating point precision, slightly over values appear
let ang = acos(max(-1, min(1, dot / len)));
if (ux * vy - uy * vx < 0) {
ang = -ang;
}
return ang;
}
// Returns true if this is a non-transforming matrix
function isDefaultMatrix(m) {
return (
m[0] === 1 &&
m[3] === 0 &&
m[1] === 0 &&
m[4] === 1 &&
m[2] === 0 &&
m[5] === 0
);
}
================================================
FILE: src/renderers/svg.d.ts
================================================
declare module 'two.js/src/renderers/svg' {
/**
* @name Two.SVGRenderer
* @class
* @param {Object} [parameters] - This object is inherited when constructing a new instance of {@link Two}.
* @param {Element} [parameters.domElement] - The `` to draw to. If none given a new one will be constructed.
* @description This class is used by {@link Two} when constructing with `type` of `Two.Types.svg` (the default type). It takes Two.js' scenegraph and renders it to a ``.
*/
export class Renderer extends Events {
/**
* @name Two.SVGRenderer.Utils
* @property {Object} - A massive object filled with utility functions and properties to render Two.js objects to a ``.
*/
static Utils: {
version: number;
ns: string;
xlink: string;
alignments: {
left: string;
center: string;
right: string;
};
createElement: (name: any, attrs: any) => SVGElement;
setAttributes: (elem: any, attrs: any) => any;
removeAttributes: (elem: any, attrs: any) => any;
toString: (points: any, closed: any) => string;
pointsToString: (points: any, size: any) => string;
getClip: (shape: any, domElement: any) => any;
group: {
appendChild: (object: any) => void;
removeChild: (object: any) => void;
orderChild: (object: any) => void;
renderChild: (child: any) => void;
render: (domElement: any) => any;
};
path: {
render: (domElement: any) => any;
};
points: {
render: (domElement: any) => any;
};
text: {
render: (domElement: any) => any;
};
'linear-gradient': {
render: (domElement: any, silent: any) => any;
};
'radial-gradient': {
render: (domElement: any, silent: any) => any;
};
texture: {
render: (domElement: any, silent: any) => any;
};
};
constructor(params?: any);
/**
* @name Two.SVGRenderer#domElement
* @property {Element} - The `` associated with the Two.js scene.
*/
domElement: any;
/**
* @name Two.SVGRenderer#scene
* @property {Group} - The root group of the scenegraph.
*/
scene: Group;
/**
* @name Two.SVGRenderer#defs
* @property {SvgDefintionsElement} - The `` to apply gradients, patterns, and bitmap imagery.
*/
defs: SVGDefsElement;
/**
* @name Two.SVGRenderer#setSize
* @function
* @param {Number} width - The new width of the renderer.
* @param {Number} height - The new height of the renderer.
* @description Change the size of the renderer.
* @nota-bene Triggers a `Two.Events.resize`.
*/
setSize(width: number, height: number): any;
width: number;
height: number;
/**
* @name Two.SVGRenderer#render
* @function
* @description Render the current scene to the ``.
*/
render(): Renderer;
}
import { Events } from 'two.js/src/events';
import { Group } from 'two.js/src/group';
}
================================================
FILE: src/renderers/svg.js
================================================
import { Commands } from '../utils/path-commands.js';
import { decomposeMatrix, mod, toFixed, getEffectiveStrokeWidth } from '../utils/math.js';
import { Events } from '../events.js';
import { _ } from '../utils/underscore.js';
import { Group } from '../group.js';
import { Vector } from '../vector.js';
const svg = {
version: 1.1,
ns: 'http://www.w3.org/2000/svg',
xlink: 'http://www.w3.org/1999/xlink',
alignments: {
left: 'start',
center: 'middle',
right: 'end',
},
baselines: {
top: 'hanging',
middle: 'middle',
bottom: 'ideographic',
baseline: 'alphabetic',
},
// Create an svg namespaced element.
createElement: function (name, attrs) {
const tag = name;
const elem = document.createElementNS(svg.ns, tag);
if (tag === 'svg') {
attrs = _.defaults(attrs || {}, {
version: svg.version,
});
}
if (attrs && Object.keys(attrs).length > 0) {
svg.setAttributes(elem, attrs);
}
return elem;
},
// Add attributes from an svg element.
setAttributes: function (elem, attrs) {
const keys = Object.keys(attrs);
for (let i = 0; i < keys.length; i++) {
if (/href/.test(keys[i])) {
elem.setAttributeNS(svg.xlink, keys[i], attrs[keys[i]]);
} else {
elem.setAttribute(keys[i], attrs[keys[i]]);
}
}
return this;
},
// Remove attributes from an svg element.
removeAttributes: function (elem, attrs) {
for (let key in attrs) {
elem.removeAttribute(key);
}
return this;
},
// Turn a set of vertices into a string for the d property of a path
// element. It is imperative that the string collation is as fast as
// possible, because this call will be happening multiple times a
// second.
toString: function (points, closed) {
let l = points.length,
last = l - 1,
d, // The elusive last Commands.move point
string = '';
for (let i = 0; i < l; i++) {
const b = points[i];
const prev = closed ? mod(i - 1, l) : Math.max(i - 1, 0);
const a = points[prev];
let command, c;
let vx, vy, ux, uy, ar, bl, br, cl;
let rx, ry, xAxisRotation, largeArcFlag, sweepFlag;
// Access x and y directly,
// bypassing the getter
let x = toFixed(b.x);
let y = toFixed(b.y);
switch (b.command) {
case Commands.close:
command = Commands.close;
break;
case Commands.arc:
rx = b.rx;
ry = b.ry;
xAxisRotation = b.xAxisRotation;
largeArcFlag = b.largeArcFlag;
sweepFlag = b.sweepFlag;
command =
Commands.arc +
' ' +
rx +
' ' +
ry +
' ' +
xAxisRotation +
' ' +
largeArcFlag +
' ' +
sweepFlag +
' ' +
x +
' ' +
y;
break;
case Commands.curve:
ar = (a.controls && a.controls.right) || Vector.zero;
bl = (b.controls && b.controls.left) || Vector.zero;
if (a.relative) {
vx = toFixed(ar.x + a.x);
vy = toFixed(ar.y + a.y);
} else {
vx = toFixed(ar.x);
vy = toFixed(ar.y);
}
if (b.relative) {
ux = toFixed(bl.x + b.x);
uy = toFixed(bl.y + b.y);
} else {
ux = toFixed(bl.x);
uy = toFixed(bl.y);
}
command =
(i === 0 ? Commands.move : Commands.curve) +
' ' +
vx +
' ' +
vy +
' ' +
ux +
' ' +
uy +
' ' +
x +
' ' +
y;
break;
case Commands.move:
d = b;
command = Commands.move + ' ' + x + ' ' + y;
break;
default:
command = b.command + ' ' + x + ' ' + y;
}
// Add a final point and close it off
if (i >= last && closed) {
if (b.command === Commands.curve) {
// Make sure we close to the most previous Commands.move
c = d;
br = (b.controls && b.controls.right) || b;
cl = (c.controls && c.controls.left) || c;
if (b.relative) {
vx = toFixed(br.x + b.x);
vy = toFixed(br.y + b.y);
} else {
vx = toFixed(br.x);
vy = toFixed(br.y);
}
if (c.relative) {
ux = toFixed(cl.x + c.x);
uy = toFixed(cl.y + c.y);
} else {
ux = toFixed(cl.x);
uy = toFixed(cl.y);
}
x = toFixed(c.x);
y = toFixed(c.y);
command +=
' C ' + vx + ' ' + vy + ' ' + ux + ' ' + uy + ' ' + x + ' ' + y;
}
if (b.command !== Commands.close) {
command += ' Z';
}
}
string += command + ' ';
}
return string;
},
pointsToString: function (points, size) {
let string = '';
const r = size * 0.5;
for (let i = 0; i < points.length; i++) {
const x = points[i].x;
const y = points[i].y - r;
string += Commands.move + ' ' + x + ' ' + y + ' ';
string += 'a ' + r + ' ' + r + ' 0 1 0 0.001 0 Z';
}
return string;
},
getClip: function (shape, domElement) {
let clip = shape._renderer.clip;
if (!clip) {
clip = shape._renderer.clip = svg.createElement('clipPath', {
'clip-rule': 'nonzero',
});
}
if (clip.parentNode === null) {
domElement.defs.appendChild(clip);
}
return clip;
},
getRendererType: function (type) {
return type in svg ? type : 'path';
},
defs: {
update: function (domElement) {
const { defs } = domElement;
if (defs._flagUpdate) {
const children = Array.prototype.slice.call(defs.children, 0);
for (let i = 0; i < children.length; i++) {
const child = children[i];
const id = child.id;
const selector = `[fill="url(#${id})"],[stroke="url(#${id})"],[clip-path="url(#${id})"]`;
const exists = domElement.querySelector(selector);
if (!exists) {
defs.removeChild(child);
}
}
defs._flagUpdate = false;
}
},
},
group: {
// TODO: Can speed up.
// TODO: How does this effect a f
appendChild: function (object) {
const elem = object._renderer.elem;
if (!elem) {
return;
}
const tag = elem.nodeName;
if (!tag || /(radial|linear)gradient/i.test(tag) || object._clip) {
return;
}
this.elem.appendChild(elem);
},
removeChild: function (object) {
const elem = object._renderer.elem;
if (!elem || elem.parentNode != this.elem) {
return;
}
const tag = elem.nodeName;
if (!tag) {
return;
}
// Defer subtractions while clipping.
if (object._clip) {
return;
}
this.elem.removeChild(elem);
},
orderChild: function (object) {
this.elem.appendChild(object._renderer.elem);
},
renderChild: function (child) {
const prop = svg.getRendererType(child._renderer.type);
svg[prop].render.call(child, this);
},
render: function (domElement) {
// Shortcut for hidden objects.
// Doesn't reset the flags, so changes are stored and
// applied once the object is visible again
if (
(!this._visible && !this._flagVisible) ||
(this._opacity === 0 && !this._flagOpacity)
) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
if (!this._renderer.elem) {
this._renderer.elem = svg.createElement('g', {
id: this.id,
});
domElement.appendChild(this._renderer.elem);
}
// _Update styles for the
const flagMatrix = this._matrix.manual || this._flagMatrix;
const context = {
domElement: domElement,
elem: this._renderer.elem,
};
if (flagMatrix) {
this._renderer.elem.setAttribute(
'transform',
'matrix(' + this._matrix.toString() + ')'
);
}
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
const prop = svg.getRendererType(child._renderer.type);
svg[prop].render.call(child, domElement);
}
if (this._flagId) {
this._renderer.elem.setAttribute('id', this._id);
}
if (this._flagOpacity) {
this._renderer.elem.setAttribute('opacity', this._opacity);
}
if (this._flagVisible) {
this._renderer.elem.setAttribute(
'display',
this._visible ? 'inline' : 'none'
);
}
if (this._flagClassName) {
this._renderer.elem.setAttribute('class', this.classList.join(' '));
}
if (this._flagAdditions) {
this.additions.forEach(svg.group.appendChild, context);
}
if (this._flagSubtractions) {
this.subtractions.forEach(svg.group.removeChild, context);
}
if (this._flagOrder) {
this.children.forEach(svg.group.orderChild, context);
}
// Commented two-way functionality of clips / masks with groups and
// polygons. Uncomment when this bug is fixed:
// https://code.google.com/p/chromium/issues/detail?id=370951
// if (this._flagClip) {
// clip = svg.getClip(this, domElement);
// elem = this._renderer.elem;
// if (this._clip) {
// elem.removeAttribute('id');
// clip.setAttribute('id', this.id);
// clip.appendChild(elem);
// } else {
// clip.removeAttribute('id');
// elem.setAttribute('id', this.id);
// this.parent._renderer.elem.appendChild(elem); // TODO: should be insertBefore
// }
// }
if (this._flagMask) {
if (this._mask) {
const prop = svg.getRendererType(this._mask._renderer.type);
svg[prop].render.call(this._mask, domElement);
this._renderer.elem.setAttribute(
'clip-path',
'url(#' + this._mask.id + ')'
);
} else {
this._renderer.elem.removeAttribute('clip-path');
}
}
if (this.dataset) {
Object.assign(this._renderer.elem.dataset, this.dataset);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
path: {
render: function (domElement) {
// Shortcut for hidden objects.
// Doesn't reset the flags, so changes are stored and
// applied once the object is visible again
if (this._opacity === 0 && !this._flagOpacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
// Collect any attribute that needs to be changed here
const changed = {};
const flagMatrix = this._matrix.manual || this._flagMatrix;
if (flagMatrix) {
changed.transform = 'matrix(' + this._matrix.toString() + ')';
}
if (this._flagId) {
changed.id = this._id;
}
if (this._flagVertices) {
const vertices = svg.toString(this._renderer.vertices, this._closed);
changed.d = vertices;
}
if (this._fill && this._fill._renderer) {
this._renderer.hasFillEffect = true;
this._fill._update();
const prop = svg.getRendererType(this._fill._renderer.type);
svg[prop].render.call(this._fill, domElement, true);
}
if (this._flagFill) {
changed.fill =
this._fill && this._fill.id
? 'url(#' + this._fill.id + ')'
: this._fill;
if (
this._renderer.hasFillEffect &&
typeof this._fill.id === 'undefined'
) {
domElement.defs._flagUpdate = true;
delete this._renderer.hasFillEffect;
}
}
if (this._stroke && this._stroke._renderer) {
this._renderer.hasStrokeEffect = true;
this._stroke._update();
const prop = svg.getRendererType(this._stroke._renderer.type);
svg[prop].render.call(this._stroke, domElement, true);
}
if (this._flagStroke) {
changed.stroke =
this._stroke && this._stroke.id
? 'url(#' + this._stroke.id + ')'
: this._stroke;
if (
this._renderer.hasStrokeEffect &&
typeof this._stroke.id === 'undefined'
) {
domElement.defs._flagUpdate = true;
delete this._renderer.hasStrokeEffect;
}
}
if (this._flagLinewidth) {
changed['stroke-width'] = getEffectiveStrokeWidth(this);
}
if (this._flagOpacity) {
changed['stroke-opacity'] = this._opacity;
changed['fill-opacity'] = this._opacity;
}
if (this._flagClassName) {
changed['class'] = this.classList.join(' ');
}
if (this._flagVisible) {
changed.visibility = this._visible ? 'visible' : 'hidden';
}
if (this._flagCap) {
changed['stroke-linecap'] = this._cap;
}
if (this._flagJoin) {
changed['stroke-linejoin'] = this._join;
}
if (this._flagMiter) {
changed['stroke-miterlimit'] = this._miter;
}
if (this.dashes && this.dashes.length > 0) {
changed['stroke-dasharray'] = this.dashes.join(' ');
changed['stroke-dashoffset'] = this.dashes.offset || 0;
}
// If there is no attached DOM element yet,
// create it with all necessary attributes.
if (!this._renderer.elem) {
changed.id = this._id;
this._renderer.elem = svg.createElement('path', changed);
domElement.appendChild(this._renderer.elem);
// Otherwise apply all pending attributes
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._flagClip) {
const clip = svg.getClip(this, domElement);
const elem = this._renderer.elem;
if (this._clip) {
elem.removeAttribute('id');
clip.setAttribute('id', this.id);
clip.appendChild(elem);
} else {
clip.removeAttribute('id');
elem.setAttribute('id', this.id);
this.parent._renderer.elem.appendChild(elem); // TODO: should be insertBefore
}
}
// Commented two-way functionality of clips / masks with groups and
// polygons. Uncomment when this bug is fixed:
// https://code.google.com/p/chromium/issues/detail?id=370951
if (this._flagMask) {
if (this._mask) {
const prop = svg.getRendererType(this._mask._renderer.type);
svg[prop].render.call(this._mask, domElement);
this._renderer.elem.setAttribute(
'clip-path',
'url(#' + this._mask.id + ')'
);
} else {
this._renderer.elem.removeAttribute('clip-path');
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
points: {
render: function (domElement) {
// Shortcut for hidden objects.
// Doesn't reset the flags, so changes are stored and
// applied once the object is visible again
if (this._opacity === 0 && !this._flagOpacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
// Collect any attribute that needs to be changed here
const changed = {};
const flagMatrix = this._matrix.manual || this._flagMatrix;
if (flagMatrix) {
changed.transform = 'matrix(' + this._matrix.toString() + ')';
}
if (this._flagId) {
changed.id = this._id;
}
if (this._flagVertices || this._flagSize || this._flagSizeAttenuation) {
let size = this._size;
if (!this._sizeAttenuation) {
const me = this.worldMatrix.elements;
const m = decomposeMatrix(me[0], me[3], me[1], me[4], me[2], me[5]);
size /= Math.max(m.scaleX, m.scaleY);
}
const vertices = svg.pointsToString(this._renderer.collection, size);
changed.d = vertices;
}
if (this._fill && this._fill._renderer) {
this._renderer.hasFillEffect = true;
this._fill._update();
const prop = svg.getRendererType(this._fill._renderer.type);
svg[prop].render.call(this._fill, domElement, true);
}
if (this._flagFill) {
changed.fill =
this._fill && this._fill.id
? 'url(#' + this._fill.id + ')'
: this._fill;
if (
this._renderer.hasFillEffect &&
typeof this._fill.id === 'undefined'
) {
domElement.defs._flagUpdate = true;
delete this._renderer.hasFillEffect;
}
}
if (this._stroke && this._stroke._renderer) {
this._renderer.hasStrokeEffect = true;
this._stroke._update();
const prop = svg.getRendererType(this._stroke._renderer.type);
svg[prop].render.call(this._stroke, domElement, true);
}
if (this._flagStroke) {
changed.stroke =
this._stroke && this._stroke.id
? 'url(#' + this._stroke.id + ')'
: this._stroke;
if (
this._renderer.hasStrokeEffect &&
typeof this._stroke.id === 'undefined'
) {
domElement.defs._flagUpdate = true;
delete this._renderer.hasStrokeEffect;
}
}
if (this._flagLinewidth) {
changed['stroke-width'] = getEffectiveStrokeWidth(this);
}
if (this._flagOpacity) {
changed['stroke-opacity'] = this._opacity;
changed['fill-opacity'] = this._opacity;
}
if (this._flagClassName) {
changed['class'] = this.classList.join(' ');
}
if (this._flagVisible) {
changed.visibility = this._visible ? 'visible' : 'hidden';
}
if (this.dashes && this.dashes.length > 0) {
changed['stroke-dasharray'] = this.dashes.join(' ');
changed['stroke-dashoffset'] = this.dashes.offset || 0;
}
// If there is no attached DOM element yet,
// create it with all necessary attributes.
if (!this._renderer.elem) {
changed.id = this._id;
this._renderer.elem = svg.createElement('path', changed);
domElement.appendChild(this._renderer.elem);
// Otherwise apply all pending attributes
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
text: {
render: function (domElement) {
this._update();
const changed = {};
const flagMatrix = this._matrix.manual || this._flagMatrix;
if (flagMatrix) {
changed.transform = 'matrix(' + this._matrix.toString() + ')';
}
if (this._flagId) {
changed.id = this._id;
}
if (this._flagFamily) {
changed['font-family'] = this._family;
}
if (this._flagSize) {
changed['font-size'] = this._size;
}
if (this._flagLeading) {
changed['line-height'] = this._leading;
}
if (this._flagAlignment) {
changed['text-anchor'] =
svg.alignments[this._alignment] || this._alignment;
}
if (this._flagBaseline) {
changed['dominant-baseline'] =
svg.baselines[this._baseline] || this._baseline;
}
if (this._flagStyle) {
changed['font-style'] = this._style;
}
if (this._flagWeight) {
changed['font-weight'] = this._weight;
}
if (this._flagDecoration) {
changed['text-decoration'] = this._decoration;
}
if (this._flagDirection) {
changed['direction'] = this._direction;
}
if (this._fill && this._fill._renderer) {
this._renderer.hasFillEffect = true;
this._fill._update();
const prop = svg.getRendererType(this._fill._renderer.type);
svg[prop].render.call(this._fill, domElement, true);
}
if (this._flagFill) {
changed.fill =
this._fill && this._fill.id
? 'url(#' + this._fill.id + ')'
: this._fill;
if (
this._renderer.hasFillEffect &&
typeof this._fill.id === 'undefined'
) {
domElement.defs._flagUpdate = true;
delete this._renderer.hasFillEffect;
}
}
if (this._stroke && this._stroke._renderer) {
this._renderer.hasStrokeEffect = true;
this._stroke._update();
const prop = svg.getRendererType(this._stroke._renderer.type);
svg[prop].render.call(this._stroke, domElement, true);
}
if (this._flagStroke) {
changed.stroke =
this._stroke && this._stroke.id
? 'url(#' + this._stroke.id + ')'
: this._stroke;
if (
this._renderer.hasStrokeEffect &&
typeof this._stroke.id === 'undefined'
) {
domElement.defs._flagUpdate = true;
delete this._renderer.hasStrokeEffect;
}
}
if (this._flagLinewidth) {
changed['stroke-width'] = getEffectiveStrokeWidth(this);
}
if (this._flagOpacity) {
changed.opacity = this._opacity;
}
if (this._flagClassName) {
changed['class'] = this.classList.join(' ');
}
if (this._flagVisible) {
changed.visibility = this._visible ? 'visible' : 'hidden';
}
if (this.dashes && this.dashes.length > 0) {
changed['stroke-dasharray'] = this.dashes.join(' ');
changed['stroke-dashoffset'] = this.dashes.offset || 0;
}
if (!this._renderer.elem) {
changed.id = this._id;
this._renderer.elem = svg.createElement('text', changed);
domElement.appendChild(this._renderer.elem);
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._flagClip) {
const clip = svg.getClip(this, domElement);
const elem = this._renderer.elem;
if (this._clip) {
elem.removeAttribute('id');
clip.setAttribute('id', this.id);
clip.appendChild(elem);
} else {
clip.removeAttribute('id');
elem.setAttribute('id', this.id);
this.parent._renderer.elem.appendChild(elem); // TODO: should be insertBefore
}
}
// Commented two-way functionality of clips / masks with groups and
// polygons. Uncomment when this bug is fixed:
// https://code.google.com/p/chromium/issues/detail?id=370951
if (this._flagMask) {
if (this._mask) {
const prop = svg.getRendererType(this._mask._renderer.type);
svg[prop].render.call(this._mask, domElement);
this._renderer.elem.setAttribute(
'clip-path',
'url(#' + this._mask.id + ')'
);
} else {
this._renderer.elem.removeAttribute('clip-path');
}
}
if (this._flagValue) {
this._renderer.elem.textContent = this._value;
}
return this.flagReset();
},
},
'linear-gradient': {
render: function (domElement, silent) {
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
if (!silent) {
this._update();
}
const changed = {};
if (this._flagId) {
changed.id = this._id;
}
if (this._flagEndPoints) {
changed.x1 = this.left._x;
changed.y1 = this.left._y;
changed.x2 = this.right._x;
changed.y2 = this.right._y;
}
if (this._flagSpread) {
changed.spreadMethod = this._spread;
}
if (this._flagUnits) {
changed.gradientUnits = this._units;
}
// If there is no attached DOM element yet,
// create it with all necessary attributes.
if (!this._renderer.elem) {
changed.id = this._id;
this._renderer.elem = svg.createElement('linearGradient', changed);
// Otherwise apply all pending attributes
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._renderer.elem.parentNode === null) {
domElement.defs.appendChild(this._renderer.elem);
}
if (this._flagStops) {
const lengthChanged =
this._renderer.elem.childNodes.length !== this.stops.length;
if (lengthChanged) {
while (this._renderer.elem.lastChild) {
this._renderer.elem.removeChild(this._renderer.elem.lastChild);
}
}
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
const attrs = {};
if (stop._flagOffset) {
attrs.offset = 100 * stop._offset + '%';
}
if (stop._flagColor) {
attrs['stop-color'] = stop._color;
}
if (stop._flagOpacity) {
attrs['stop-opacity'] = stop._opacity;
}
if (!stop._renderer.elem) {
stop._renderer.elem = svg.createElement('stop', attrs);
} else {
svg.setAttributes(stop._renderer.elem, attrs);
}
if (lengthChanged) {
this._renderer.elem.appendChild(stop._renderer.elem);
}
stop.flagReset();
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
'radial-gradient': {
render: function (domElement, silent) {
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
if (!silent) {
this._update();
}
const changed = {};
if (this._flagId) {
changed.id = this._id;
}
if (this._flagCenter) {
changed.cx = this.center._x;
changed.cy = this.center._y;
}
if (this._flagFocal) {
changed.fx = this.focal._x;
changed.fy = this.focal._y;
}
if (this._flagRadius) {
changed.r = this._radius;
}
if (this._flagSpread) {
changed.spreadMethod = this._spread;
}
if (this._flagUnits) {
changed.gradientUnits = this._units;
}
// If there is no attached DOM element yet,
// create it with all necessary attributes.
if (!this._renderer.elem) {
changed.id = this._id;
this._renderer.elem = svg.createElement('radialGradient', changed);
// Otherwise apply all pending attributes
} else {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._renderer.elem.parentNode === null) {
domElement.defs.appendChild(this._renderer.elem);
}
if (this._flagStops) {
const lengthChanged =
this._renderer.elem.childNodes.length !== this.stops.length;
if (lengthChanged) {
while (this._renderer.elem.lastChild) {
this._renderer.elem.removeChild(this._renderer.elem.lastChild);
}
}
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
const attrs = {};
if (stop._flagOffset) {
attrs.offset = 100 * stop._offset + '%';
}
if (stop._flagColor) {
attrs['stop-color'] = stop._color;
}
if (stop._flagOpacity) {
attrs['stop-opacity'] = stop._opacity;
}
if (!stop._renderer.elem) {
stop._renderer.elem = svg.createElement('stop', attrs);
} else {
svg.setAttributes(stop._renderer.elem, attrs);
}
if (lengthChanged) {
this._renderer.elem.appendChild(stop._renderer.elem);
}
stop.flagReset();
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
texture: {
render: function (domElement, silent) {
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
if (!silent) {
this._update();
}
const changed = {};
const styles = { x: 0, y: 0 };
const image = this.image;
if (this._flagId) {
changed.id = this._id;
}
if (this._flagLoaded && this.loaded) {
switch (image.nodeName.toLowerCase()) {
case 'canvas':
styles.href = styles['xlink:href'] = image.toDataURL('image/png');
break;
case 'img':
case 'image':
styles.href = styles['xlink:href'] = this.src;
break;
}
}
if (this._flagOffset || this._flagLoaded || this._flagScale) {
changed.x = this._offset.x;
changed.y = this._offset.y;
if (image) {
changed.x -= image.width / 2;
changed.y -= image.height / 2;
if (this._scale instanceof Vector) {
changed.x *= this._scale.x;
changed.y *= this._scale.y;
} else {
changed.x *= this._scale;
changed.y *= this._scale;
}
}
if (changed.x > 0) {
changed.x *= -1;
}
if (changed.y > 0) {
changed.y *= -1;
}
}
if (this._flagScale || this._flagLoaded || this._flagRepeat) {
changed.width = 0;
changed.height = 0;
if (image) {
changed.width = image.width;
changed.height = image.height;
// TODO: Hack / Band-aid
switch (this._repeat) {
case 'no-repeat':
changed.width += 1;
changed.height += 1;
break;
}
if (this._scale instanceof Vector) {
changed.width *= this._scale.x;
changed.height *= this._scale.y;
} else {
changed.width *= this._scale;
changed.height *= this._scale;
}
if (/no-repeat/i.test(this._repeat)) {
styles.preserveAspectRatio = 'xMidYMid';
} else {
styles.preserveAspectRatio = 'none';
}
styles.width = changed.width;
styles.height = changed.height;
}
}
if (this._flagScale || this._flagLoaded) {
if (!this._renderer.image) {
this._renderer.image = svg.createElement('image', styles);
} else {
svg.setAttributes(this._renderer.image, styles);
}
}
if (!this._renderer.elem) {
changed.id = this._id;
changed.patternUnits = 'userSpaceOnUse';
this._renderer.elem = svg.createElement('pattern', changed);
} else if (Object.keys(changed).length !== 0) {
svg.setAttributes(this._renderer.elem, changed);
}
if (this._renderer.elem.parentNode === null) {
domElement.defs.appendChild(this._renderer.elem);
}
if (
this._renderer.elem &&
this._renderer.image &&
!this._renderer.appended
) {
this._renderer.elem.appendChild(this._renderer.image);
this._renderer.appended = true;
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
};
/**
* @name Two.SVGRenderer
* @class
* @extends Two.Events
* @param {Object} [parameters] - This object is inherited when constructing a new instance of {@link Two}.
* @param {Element} [parameters.domElement] - The `` to draw to. If none given a new one will be constructed.
* @description This class is used by {@link Two} when constructing with `type` of `Two.Types.svg` (the default type). It takes Two.js' scenegraph and renders it to a ``.
*/
export class Renderer extends Events {
constructor(params) {
super();
/**
* @name Two.SVGRenderer#domElement
* @property {Element} - The `` associated with the Two.js scene.
*/
this.domElement = params.domElement || svg.createElement('svg');
/**
* @name Two.SVGRenderer#scene
* @property {Two.Group} - The root group of the scenegraph.
*/
this.scene = new Group();
this.scene.parent = this;
/**
* @name Two.SVGRenderer#defs
* @property {SvgDefintionsElement} - The `` to apply gradients, patterns, and bitmap imagery.
*/
this.defs = svg.createElement('defs');
this.defs._flagUpdate = false;
this.domElement.appendChild(this.defs);
this.domElement.defs = this.defs;
this.domElement.style.overflow = 'hidden';
}
/**
* @name Two.SVGRenderer.Utils
* @property {Object} - A massive object filled with utility functions and properties to render Two.js objects to a ``.
*/
static Utils = svg;
/**
* @name Two.SVGRenderer#setSize
* @function
* @param {Number} width - The new width of the renderer.
* @param {Number} height - The new height of the renderer.
* @description Change the size of the renderer.
* @nota-bene Triggers a `Two.Events.resize`.
*/
setSize(width, height) {
this.width = width;
this.height = height;
svg.setAttributes(this.domElement, {
width: width,
height: height,
});
return this.trigger(Events.Types.resize, width, height);
}
/**
* @name Two.SVGRenderer#render
* @function
* @description Render the current scene to the ``.
*/
render() {
svg.group.render.call(this.scene, this.domElement);
svg.defs.update(this.domElement);
return this;
}
}
================================================
FILE: src/renderers/webgl.d.ts
================================================
declare module 'two.js/src/renderers/webgl' {
/**
* @name Two.WebGLRenderer
* @class
* @param {Object} [parameters] - This object is inherited when constructing a new instance of {@link Two}.
* @param {Element} [parameters.domElement] - The `` to draw to. If none given a new one will be constructed.
* @param {HTMLCanvasElement} [parameters.offscreenElement] - The offscreen two dimensional `` to render each element on WebGL texture updates.
* @param {Boolean} [parameters.antialias] - Determines whether the canvas should clear render with antialias on.
* @description This class is used by {@link Two} when constructing with `type` of `Two.Types.webgl`. It takes Two.js' scenegraph and renders it to a `` through the WebGL api.
* @see {@link https://www.khronos.org/registry/webgl/specs/latest/1.0/}
*/
export class Renderer extends Events {
/**
* @name Two.WebGLRenderer.Utils
* @property {Object} - A massive object filled with utility functions and properties to render Two.js objects to a `` through the WebGL API.
*/
static Utils: {
precision: number;
isHidden: RegExp;
canvas: any;
alignments: {
left: string;
middle: string;
right: string;
};
matrix: Matrix;
group: {
removeChild: (child: any, gl: any) => void;
/**
* @function
// * @type {(gl: any, programs: any) => any}
* @param {WebGLContext} gl
* @param {Object} programs
*/
render: (gl: any, programs: any) => any;
};
path: {
updateCanvas: (elem: any) => void;
getBoundingClientRect: (vertices: any, border: any, rect: any) => void;
render: (gl: any, programs: any, forcedParent: any) => any;
};
points: {
updateCanvas: (elem: any) => void;
render: (gl: any, programs: any, forcedParent: any) => any;
};
text: {
updateCanvas: (elem: any) => void;
getBoundingClientRect: (elem: any, rect: any) => void;
render: (gl: any, programs: any, forcedParent: any) => any;
};
'linear-gradient': {
render: (ctx: any, parent: any) => any;
};
'radial-gradient': {
render: (ctx: any, parent: any) => any;
};
texture: {
render: (ctx: any, elem: any) => any;
};
updateTexture: (gl: any, elem: any) => void;
program: {
create: (gl: any, shaders: any) => any;
};
TextureRegistry: Registry;
};
constructor(params?: any);
/**
* @name Two.WebGLRenderer#domElement
* @property {Element} - The `` associated with the Two.js scene.
*/
domElement: HTMLCanvasElement;
/**
* @name Two.WebGLRenderer#scene
* @property {Group} - The root group of the scenegraph.
*/
scene: Group;
_renderer: {
type: string;
matrix: number[];
scale: number;
opacity: number;
};
_flagMatrix: boolean;
/**
* @name Two.WebGLRenderer#overdraw
* @property {Boolean} - Determines whether the canvas clears the background each draw call.
* @default true
*/
overdraw: any;
ctx: any;
/**
* @name Two.WebGLRenderer#programs
* @property {Object} - Associated WebGL programs to render all elements from the scenegraph.
*/
programs: {
current: any;
buffers: {
position: any;
};
resolution: {
width: number;
height: number;
ratio: number;
flagged: boolean;
};
};
/**
* @name Two.WebGLRenderer#setSize
* @function
* @fires resize
* @param {Number} width - The new width of the renderer.
* @param {Number} height - The new height of the renderer.
* @param {Number} [ratio] - The new pixel ratio (pixel density) of the renderer. Defaults to calculate the pixel density of the user's screen.
* @description Change the size of the renderer.
*/
setSize(width: number, height: number, ratio?: number): any;
width: number;
height: number;
ratio: number;
/**
* @name Two.WebGLRenderer#render
* @function
* @description Render the current scene to the ``.
*/
render(): Renderer;
}
import { Events } from 'two.js/src/events';
import { Group } from 'two.js/src/group';
import { Matrix } from 'two.js/src/matrix';
import { Registry } from 'two.js/src/registry';
}
================================================
FILE: src/renderers/webgl.js
================================================
import { Commands } from '../utils/path-commands.js';
import { root } from '../utils/root.js';
import {
getPoT,
mod,
NumArray,
TWO_PI,
getEffectiveStrokeWidth,
} from '../utils/math.js';
import { shaders } from '../utils/shaders.js';
import { Events } from '../events.js';
import { TwoError } from '../utils/error.js';
import { getRatio } from '../utils/device-pixel-ratio.js';
import { _ } from '../utils/underscore.js';
import { Group } from '../group.js';
import { Vector } from '../vector.js';
import { Matrix } from '../matrix.js';
import { Registry } from '../registry.js';
import { LinearGradient } from '../effects/linear-gradient.js';
import { RadialGradient } from '../effects/radial-gradient.js';
import { Texture } from '../effects/texture.js';
import { Renderer as CanvasRenderer } from './canvas.js';
// Constants
const multiplyMatrix = Matrix.Multiply,
identity = [1, 0, 0, 0, 1, 0, 0, 0, 1],
transformation = new NumArray(9),
CanvasUtils = CanvasRenderer.Utils,
vector = new Vector();
const quad = new NumArray([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]);
const webgl = {
precision: 0.9,
isHidden: /(undefined|none|transparent)/i,
canvas: root.document
? root.document.createElement('canvas')
: { getContext: function () {} },
alignments: {
left: 'start',
middle: 'center',
right: 'end',
},
matrix: new Matrix(),
group: {
removeChild: function (child, gl) {
if (child.children) {
for (let i = 0; i < child.children.length; i++) {
webgl.group.removeChild(child.children[i], gl);
}
}
// Deallocate texture to free up gl memory.
if (child._renderer.texture) {
gl.deleteTexture(child._renderer.texture);
delete child._renderer.texture;
}
// Deallocate vertices to free up gl memory.
if (child._renderer.positionBuffer) {
gl.deleteBuffer(child._renderer.positionBuffer);
delete child._renderer.positionBuffer;
}
},
/**
* @function
// * @type {(gl: any, programs: any) => any}
* @param {WebGLContext} gl
* @param {Object} programs
*/
render: function (gl, programs) {
if (!this._visible) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const parent = this.parent;
const flagParentMatrix =
(parent._matrix && parent._matrix.manual) || parent._flagMatrix;
const flagMatrix = this._matrix.manual || this._flagMatrix;
if (flagParentMatrix || flagMatrix) {
if (!this._renderer.matrix) {
this._renderer.matrix = new NumArray(9);
}
// Reduce amount of object / array creation / deletion
this._matrix.toTransformArray(true, transformation);
multiplyMatrix(
transformation,
parent._renderer.matrix,
this._renderer.matrix,
);
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
if (this._scale instanceof Vector) {
this._renderer.scale.x = this._scale.x;
this._renderer.scale.y = this._scale.y;
} else {
this._renderer.scale.x = this._scale;
this._renderer.scale.y = this._scale;
}
if (!/renderer/i.test(parent._renderer.type)) {
this._renderer.scale.x *= parent._renderer.scale.x;
this._renderer.scale.y *= parent._renderer.scale.y;
}
if (flagParentMatrix) {
this._flagMatrix = true;
}
}
if (this._mask) {
// Stencil away everything that isn't rendered by the mask
gl.clear(gl.STENCIL_BUFFER_BIT);
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.ALWAYS, 1, 0);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
// Don't draw the element onto the canvas, only onto the stencil buffer
gl.colorMask(false, false, false, false);
const prop = CanvasRenderer.Utils.getRendererType(
this._mask._renderer.type,
);
webgl[prop].render.call(this._mask, gl, programs, this);
gl.stencilFunc(gl.EQUAL, 1, 0xff);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
gl.colorMask(true, true, true, true);
}
this._flagOpacity = parent._flagOpacity || this._flagOpacity;
this._renderer.opacity =
this._opacity *
(parent && parent._renderer ? parent._renderer.opacity : 1);
let i;
if (this._flagSubtractions) {
for (i = 0; i < this.subtractions.length; i++) {
webgl.group.removeChild(this.subtractions[i], gl);
}
}
for (i = 0; i < this.children.length; i++) {
const child = this.children[i];
const prop = CanvasRenderer.Utils.getRendererType(child._renderer.type);
webgl[prop].render.call(child, gl, programs);
}
if (this._mask) {
gl.disable(gl.STENCIL_TEST);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
path: {
updateCanvas: function (gl, elem) {
let prev, a, c, ux, uy, vx, vy, ar, bl, br, cl, x, y;
let isOffset;
const commands = elem._renderer.vertices;
const canvas = this.canvas;
const ctx = this.ctx;
const ratio = gl.renderer.ratio;
// Styles
const scale = vector.copy(elem._renderer.scale).multiply(ratio);
const stroke = elem._stroke;
const linewidth = elem._linewidth;
const fill = elem._fill;
const opacity = elem._renderer.opacity || elem._opacity;
const cap = elem._cap;
const join = elem._join;
const miter = elem._miter;
const closed = elem._closed;
const dashes = elem.dashes;
const length = commands.length;
const last = length - 1;
canvas.width = Math.max(
Math.ceil(elem._renderer.rect.width * scale.x),
1,
);
canvas.height = Math.max(
Math.ceil(elem._renderer.rect.height * scale.y),
1,
);
const centroid = elem._renderer.rect.centroid;
const cx = centroid.x;
const cy = centroid.y;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (fill) {
if (typeof fill === 'string') {
ctx.fillStyle = fill;
} else {
const prop = CanvasRenderer.Utils.getRendererType(
fill._renderer.type,
);
webgl[prop].render.call(fill, ctx, elem);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === 'string') {
ctx.strokeStyle = stroke;
} else {
const prop = CanvasRenderer.Utils.getRendererType(
stroke._renderer.type,
);
webgl[prop].render.call(stroke, ctx, elem);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(elem);
}
if (miter) {
ctx.miterLimit = miter;
}
if (join) {
ctx.lineJoin = join;
}
if (!closed && cap) {
ctx.lineCap = cap;
}
}
if (typeof opacity === 'number') {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
let d, rx, ry, xAxisRotation, largeArcFlag, sweepFlag, ax, ay;
ctx.save();
ctx.scale(scale.x, scale.y);
ctx.translate(cx, cy);
ctx.beginPath();
for (let i = 0; i < commands.length; i++) {
const b = commands[i];
x = b.x;
y = b.y;
switch (b.command) {
case Commands.close:
ctx.closePath();
break;
case Commands.arc:
rx = b.rx;
ry = b.ry;
xAxisRotation = b.xAxisRotation;
largeArcFlag = b.largeArcFlag;
sweepFlag = b.sweepFlag;
prev = closed ? mod(i - 1, length) : Math.max(i - 1, 0);
a = commands[prev];
ax = a.x;
ay = a.y;
CanvasUtils.renderSvgArcCommand(
ctx,
ax,
ay,
rx,
ry,
largeArcFlag,
sweepFlag,
xAxisRotation,
x,
y,
);
break;
case Commands.curve:
prev = closed ? mod(i - 1, length) : Math.max(i - 1, 0);
a = commands[prev];
ar = (a.controls && a.controls.right) || Vector.zero;
bl = (b.controls && b.controls.left) || Vector.zero;
if (a._relative) {
vx = ar.x + a.x;
vy = ar.y + a.y;
} else {
vx = ar.x;
vy = ar.y;
}
if (b._relative) {
ux = bl.x + b.x;
uy = bl.y + b.y;
} else {
ux = bl.x;
uy = bl.y;
}
ctx.bezierCurveTo(vx, vy, ux, uy, x, y);
if (i >= last && closed) {
c = d;
br = (b.controls && b.controls.right) || Vector.zero;
cl = (c.controls && c.controls.left) || Vector.zero;
if (b._relative) {
vx = br.x + b.x;
vy = br.y + b.y;
} else {
vx = br.x;
vy = br.y;
}
if (c._relative) {
ux = cl.x + c.x;
uy = cl.y + c.y;
} else {
ux = cl.x;
uy = cl.y;
}
x = c.x;
y = c.y;
ctx.bezierCurveTo(vx, vy, ux, uy, x, y);
}
break;
case Commands.line:
ctx.lineTo(x, y);
break;
case Commands.move:
d = b;
ctx.moveTo(x, y);
break;
}
}
// Loose ends
if (closed) {
ctx.closePath();
}
if (!webgl.isHidden.test(fill)) {
isOffset = fill._renderer && fill._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(fill._renderer.scale.x, fill._renderer.scale.y);
}
ctx.fill();
if (isOffset) {
ctx.restore();
}
}
if (!webgl.isHidden.test(stroke)) {
isOffset = stroke._renderer && stroke._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-stroke._renderer.offset.x, -stroke._renderer.offset.y);
ctx.scale(stroke._renderer.scale.x, stroke._renderer.scale.y);
ctx.lineWidth = linewidth / stroke._renderer.scale.x;
}
ctx.stroke();
if (isOffset) {
ctx.restore();
}
}
ctx.restore();
},
// Returns the rect of a set of verts. Typically takes vertices that are
// "centered" around 0 and returns them to be anchored upper-left.
getBoundingClientRect: function (vertices, border, rect) {
let left = Infinity,
right = -Infinity,
top = Infinity,
bottom = -Infinity,
width,
height;
vertices.forEach(function (v) {
const x = v.x,
y = v.y,
controls = v.controls;
let a, b, c, d, cl, cr;
top = Math.min(y, top);
left = Math.min(x, left);
right = Math.max(x, right);
bottom = Math.max(y, bottom);
if (!v.controls) {
return;
}
cl = controls.left;
cr = controls.right;
if (!cl || !cr) {
return;
}
a = v._relative ? cl.x + x : cl.x;
b = v._relative ? cl.y + y : cl.y;
c = v._relative ? cr.x + x : cr.x;
d = v._relative ? cr.y + y : cr.y;
if (!a || !b || !c || !d) {
return;
}
top = Math.min(b, d, top);
left = Math.min(a, c, left);
right = Math.max(a, c, right);
bottom = Math.max(b, d, bottom);
});
// Expand borders
if (typeof border === 'number') {
top -= border;
left -= border;
right += border;
bottom += border;
}
width = right - left;
height = bottom - top;
rect.top = top;
rect.left = left;
rect.right = right;
rect.bottom = bottom;
rect.width = width;
rect.height = height;
if (!rect.centroid) {
rect.centroid = {};
}
rect.centroid.x = -left;
rect.centroid.y = -top;
},
render: function (gl, programs, forcedParent) {
if (!this._visible || !this._opacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
// Calculate what changed
const parent = forcedParent || this.parent;
const prop = CanvasRenderer.Utils.getRendererType(this._renderer.type);
const program = programs[prop];
const flagParentMatrix = parent._matrix.manual || parent._flagMatrix;
const flagMatrix = this._matrix.manual || this._flagMatrix;
const parentChanged = this._renderer.parent !== parent;
const flagTexture =
this._flagVertices ||
this._flagFill ||
(this._fill instanceof LinearGradient &&
(this._fill._flagSpread ||
this._fill._flagStops ||
this._fill._flagEndPoints)) ||
(this._fill instanceof RadialGradient &&
(this._fill._flagSpread ||
this._fill._flagStops ||
this._fill._flagRadius ||
this._fill._flagCenter ||
this._fill._flagFocal)) ||
(this._fill instanceof Texture &&
((this._fill._flagLoaded && this._fill.loaded) ||
this._fill._flagImage ||
this._fill._flagVideo ||
this._fill._flagRepeat ||
this._fill._flagOffset ||
this._fill._flagScale)) ||
(this._stroke instanceof LinearGradient &&
(this._stroke._flagSpread ||
this._stroke._flagStops ||
this._stroke._flagEndPoints)) ||
(this._stroke instanceof RadialGradient &&
(this._stroke._flagSpread ||
this._stroke._flagStops ||
this._stroke._flagRadius ||
this._stroke._flagCenter ||
this._stroke._flagFocal)) ||
(this._stroke instanceof Texture &&
((this._stroke._flagLoaded && this._stroke.loaded) ||
this._stroke._flagImage ||
this._stroke._flagVideo ||
this._stroke._flagRepeat ||
this._stroke._flagOffset ||
this._fill._flagScale)) ||
this._flagStroke ||
this._flagLinewidth ||
this._flagOpacity ||
parent._flagOpacity ||
this._flagVisible ||
this._flagCap ||
this._flagJoin ||
this._flagMiter ||
this._flagScale ||
(this.dashes && this.dashes.length > 0) ||
!this._renderer.texture;
if (flagParentMatrix || flagMatrix || parentChanged) {
if (!this._renderer.matrix) {
this._renderer.matrix = new NumArray(9);
}
// Reduce amount of object / array creation / deletion
this._matrix.toTransformArray(true, transformation);
multiplyMatrix(
transformation,
parent._renderer.matrix,
this._renderer.matrix,
);
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
let sx, sy;
if (this._scale instanceof Vector) {
sx = this._scale.x * parent._renderer.scale.x;
sy = this._scale.y * parent._renderer.scale.y;
} else {
sx = this._scale * parent._renderer.scale.x;
sy = this._scale * parent._renderer.scale.y;
}
this._renderer.scale.x = sx < 0 ? -sx : sx;
this._renderer.scale.y = sy < 0 ? -sy : sy;
if (parentChanged) {
this._renderer.parent = parent;
}
}
if (this._mask) {
// Stencil away everything that isn't rendered by the mask
gl.clear(gl.STENCIL_BUFFER_BIT);
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.ALWAYS, 1, 0);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
// Don't draw the element onto the canvas, only onto the stencil buffer
gl.colorMask(false, false, false, false);
const prop = CanvasRenderer.Utils.getRendererType(
this._mask._renderer.type,
);
webgl[prop].render.call(this._mask, gl, programs, this);
gl.stencilFunc(gl.EQUAL, 1, 0xff);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
gl.colorMask(true, true, true, true);
}
if (flagTexture) {
if (!this._renderer.rect) {
this._renderer.rect = {};
}
this._renderer.opacity = this._opacity * parent._renderer.opacity;
webgl.path.getBoundingClientRect(
this._renderer.vertices,
this._linewidth,
this._renderer.rect,
);
webgl.updateTexture.call(webgl, gl, this);
} else {
// We still need to update child Two elements on the fill and
// stroke properties.
if (this._fill && this._fill._update) {
this._fill._update();
}
if (this._stroke && this._stroke._update) {
this._stroke._update();
}
}
if ((this._clip && !forcedParent) || !this._renderer.texture) {
return this;
}
if (programs.current !== program) {
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, programs.buffers.position);
gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.position);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
if (!programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, 'u_resolution'),
programs.resolution.width,
programs.resolution.height,
);
}
programs.current = program;
}
if (programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, 'u_resolution'),
programs.resolution.width,
programs.resolution.height,
);
}
// Draw Texture
gl.bindTexture(gl.TEXTURE_2D, this._renderer.texture);
// Draw Rect
const rect = this._renderer.rect;
gl.uniformMatrix3fv(program.matrix, false, this._renderer.matrix);
gl.uniform4f(program.rect, rect.left, rect.top, rect.right, rect.bottom);
gl.drawArrays(gl.TRIANGLES, 0, 6);
if (this._mask) {
gl.disable(gl.STENCIL_TEST);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
points: {
// The canvas is a texture that is a rendering of one vertex
updateCanvas: function (gl, elem) {
let isOffset;
const canvas = this.canvas;
const ctx = this.ctx;
const ratio = gl.renderer.ratio;
// Styles
const stroke = elem._stroke;
const linewidth = elem._linewidth;
const fill = elem._fill;
const opacity = elem._renderer.opacity || elem._opacity;
const dashes = elem.dashes;
const size = elem._size * ratio;
const closed = elem._closed;
let dimension = size;
if (!webgl.isHidden.test(stroke)) {
dimension += linewidth;
}
canvas.width = getPoT(dimension);
canvas.height = canvas.width;
const aspect = dimension / canvas.width;
const cx = canvas.width / 2;
const cy = canvas.height / 2;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (fill) {
if (typeof fill === 'string') {
ctx.fillStyle = fill;
} else {
const prop = CanvasRenderer.Utils.getRendererType(
fill._renderer.type,
);
webgl[prop].render.call(fill, ctx, elem);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === 'string') {
ctx.strokeStyle = stroke;
} else {
const prop = CanvasRenderer.Utils.getRendererType(
stroke._renderer.type,
);
webgl[prop].render.call(stroke, ctx, elem);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(elem) / aspect;
}
}
if (typeof opacity === 'number') {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
ctx.save();
ctx.translate(cx, cy);
ctx.scale(webgl.precision, webgl.precision); // Precision for even rendering
ctx.beginPath();
ctx.arc(0, 0, (size / aspect) * 0.5, 0, TWO_PI);
ctx.restore();
// Loose ends
if (closed) {
ctx.closePath();
}
if (!webgl.isHidden.test(fill)) {
isOffset = fill._renderer && fill._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(fill._renderer.scale.x, fill._renderer.scale.y);
}
ctx.fill();
if (isOffset) {
ctx.restore();
}
}
if (!webgl.isHidden.test(stroke)) {
isOffset = stroke._renderer && stroke._renderer.offset;
if (isOffset) {
ctx.save();
ctx.translate(-stroke._renderer.offset.x, -stroke._renderer.offset.y);
ctx.scale(stroke._renderer.scale.x, stroke._renderer.scale.y);
ctx.lineWidth = linewidth / stroke._renderer.scale.x;
}
ctx.stroke();
if (isOffset) {
ctx.restore();
}
}
},
render: function (gl, programs, forcedParent) {
if (!this._visible || !this._opacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
// Calculate what changed
let size = this._size;
const parent = forcedParent || this.parent;
const program = programs[this._renderer.type];
const sizeAttenuation = this._sizeAttenuation;
const stroke = this._stroke;
const linewidth = this._linewidth;
const flagParentMatrix = parent._matrix.manual || parent._flagMatrix;
const flagMatrix = this._matrix.manual || this._flagMatrix;
const parentChanged = this._renderer.parent !== parent;
const commands = this._renderer.vertices;
const length = this._renderer.collection.length;
const flagVertices = this._flagVertices;
const flagTexture =
this._flagFill ||
(this._fill instanceof LinearGradient &&
(this._fill._flagSpread ||
this._fill._flagStops ||
this._fill._flagEndPoints)) ||
(this._fill instanceof RadialGradient &&
(this._fill._flagSpread ||
this._fill._flagStops ||
this._fill._flagRadius ||
this._fill._flagCenter ||
this._fill._flagFocal)) ||
(this._fill instanceof Texture &&
((this._fill._flagLoaded && this._fill.loaded) ||
this._fill._flagImage ||
this._fill._flagVideo ||
this._fill._flagRepeat ||
this._fill._flagOffset ||
this._fill._flagScale)) ||
(this._stroke instanceof LinearGradient &&
(this._stroke._flagSpread ||
this._stroke._flagStops ||
this._stroke._flagEndPoints)) ||
(this._stroke instanceof RadialGradient &&
(this._stroke._flagSpread ||
this._stroke._flagStops ||
this._stroke._flagRadius ||
this._stroke._flagCenter ||
this._stroke._flagFocal)) ||
(this._stroke instanceof Texture &&
((this._stroke._flagLoaded && this._stroke.loaded) ||
this._stroke._flagImage ||
this._stroke._flagVideo ||
this._stroke._flagRepeat ||
this._stroke._flagOffset ||
this._fill._flagScale)) ||
this._flagStroke ||
this._flagLinewidth ||
this._flagOpacity ||
parent._flagOpacity ||
this._flagVisible ||
this._flagScale ||
(this.dashes && this.dashes.length > 0) ||
!this._renderer.texture;
if (flagParentMatrix || flagMatrix || parentChanged) {
if (!this._renderer.matrix) {
this._renderer.matrix = new NumArray(9);
}
// Reduce amount of object / array creation / deletion
this._matrix.toTransformArray(true, transformation);
multiplyMatrix(
transformation,
parent._renderer.matrix,
this._renderer.matrix,
);
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
let sx, sy;
if (this._scale instanceof Vector) {
sx = this._scale.x * parent._renderer.scale.x;
sy = this._scale.y * parent._renderer.scale.y;
} else {
sx = this._scale * parent._renderer.scale.x;
sy = this._scale * parent._renderer.scale.y;
}
this._renderer.scale.x = sx < 0 ? -sx : sx;
this._renderer.scale.y = sy < 0 ? -sy : sy;
if (parentChanged) {
this._renderer.parent = parent;
}
}
if (flagVertices) {
const positionBuffer = this._renderer.positionBuffer;
if (positionBuffer) {
gl.deleteBuffer(positionBuffer);
}
// Bind the vertex buffer
this._renderer.positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this._renderer.positionBuffer);
gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.position);
gl.bufferData(gl.ARRAY_BUFFER, commands, gl.STATIC_DRAW);
}
if (flagTexture) {
this._renderer.opacity = this._opacity * parent._renderer.opacity;
webgl.updateTexture.call(webgl, gl, this);
} else {
// We still need to update child Two elements on the fill and
// stroke properties.
if (this._fill && this._fill._update) {
this._fill._update();
}
if (this._stroke && this._stroke._update) {
this._stroke._update();
}
}
if ((this._clip && !forcedParent) || !this._renderer.texture) {
return this;
}
if (!webgl.isHidden.test(stroke)) {
size += linewidth;
}
size /= webgl.precision;
if (sizeAttenuation) {
size *= Math.max(this._renderer.scale.x, this._renderer.scale.y);
}
if (programs.current !== program) {
gl.useProgram(program);
if (!programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, 'u_resolution'),
programs.resolution.width,
programs.resolution.height,
);
}
programs.current = program;
}
if (programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, 'u_resolution'),
programs.resolution.width,
programs.resolution.height,
);
}
// Draw Texture
gl.bindTexture(gl.TEXTURE_2D, this._renderer.texture);
// Draw Points
gl.uniformMatrix3fv(program.matrix, false, this._renderer.matrix);
gl.uniform1f(program.size, size * programs.resolution.ratio);
gl.drawArrays(gl.POINTS, 0, length);
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
text: {
updateCanvas: function (gl, elem) {
const canvas = this.canvas;
const ctx = this.ctx;
const ratio = gl.renderer.ratio;
// Styles
const scale = vector.copy(elem._renderer.scale).multiply(ratio);
const stroke = elem._stroke;
const linewidth = elem._linewidth;
const fill = elem._fill;
const opacity = elem._renderer.opacity || elem._opacity;
const dashes = elem.dashes;
const decoration = elem._decoration;
const direction = elem._direction;
canvas.width = Math.max(
Math.ceil(elem._renderer.rect.width * scale.x),
1,
);
canvas.height = Math.max(
Math.ceil(elem._renderer.rect.height * scale.y),
1,
);
const centroid = elem._renderer.rect.centroid;
const cx = centroid.x;
const cy = centroid.y;
let a, b, c, d, e, sx, sy, x1, y1, x2, y2;
const isOffset =
fill._renderer &&
fill._renderer.offset &&
stroke._renderer &&
stroke._renderer.offset;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!isOffset) {
ctx.font = [
elem._style,
elem._weight,
elem._size + 'px/' + elem._leading + 'px',
elem._family,
].join(' ');
}
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.textDirection = direction;
// Styles
if (fill) {
if (typeof fill === 'string') {
ctx.fillStyle = fill;
} else {
const prop = CanvasRenderer.Utils.getRendererType(
fill._renderer.type,
);
webgl[prop].render.call(fill, ctx, elem);
ctx.fillStyle = fill._renderer.effect;
}
}
if (stroke) {
if (typeof stroke === 'string') {
ctx.strokeStyle = stroke;
} else {
const prop = CanvasRenderer.Utils.getRendererType(
stroke._renderer.type,
);
webgl[prop].render.call(stroke, ctx, elem);
ctx.strokeStyle = stroke._renderer.effect;
}
if (linewidth) {
ctx.lineWidth = getEffectiveStrokeWidth(elem);
}
}
if (typeof opacity === 'number') {
ctx.globalAlpha = opacity;
}
if (dashes && dashes.length > 0) {
ctx.lineDashOffset = dashes.offset || 0;
ctx.setLineDash(dashes);
}
ctx.save();
ctx.scale(scale.x, scale.y);
ctx.translate(cx, cy);
if (!webgl.isHidden.test(fill)) {
if (fill._renderer && fill._renderer.offset) {
sx = fill._renderer.scale.x;
sy = fill._renderer.scale.y;
ctx.save();
ctx.translate(-fill._renderer.offset.x, -fill._renderer.offset.y);
ctx.scale(sx, sy);
a = elem._size / fill._renderer.scale.y;
b = elem._leading / fill._renderer.scale.y;
ctx.font = [
elem._style,
elem._weight,
a + 'px/',
b + 'px',
elem._family,
].join(' ');
c = fill._renderer.offset.x / fill._renderer.scale.x;
d = fill._renderer.offset.y / fill._renderer.scale.y;
ctx.fillText(elem.value, c, d);
ctx.restore();
} else {
ctx.fillText(elem.value, 0, 0);
}
}
if (!webgl.isHidden.test(stroke)) {
if (stroke._renderer && stroke._renderer.offset) {
sx = stroke._renderer.scale.x;
sy = stroke._renderer.scale.y;
ctx.save();
ctx.translate(-stroke._renderer.offset.x, -stroke._renderer.offset.y);
ctx.scale(sx, sy);
a = elem._size / stroke._renderer.scale.y;
b = elem._leading / stroke._renderer.scale.y;
ctx.font = [
elem._style,
elem._weight,
a + 'px/',
b + 'px',
elem._family,
].join(' ');
c = stroke._renderer.offset.x / stroke._renderer.scale.x;
d = stroke._renderer.offset.y / stroke._renderer.scale.y;
e = linewidth / stroke._renderer.scale.x;
ctx.lineWidth = e;
ctx.strokeText(elem.value, c, d);
ctx.restore();
} else {
ctx.strokeText(elem.value, 0, 0);
}
}
// Handle text-decoration
if (/(underline|strikethrough)/i.test(decoration)) {
const metrics = ctx.measureText(elem.value);
switch (decoration) {
case 'underline':
y1 = metrics.actualBoundingBoxDescent;
y2 = metrics.actualBoundingBoxDescent;
break;
case 'strikethrough':
y1 = 0;
y2 = 0;
break;
}
x1 = -metrics.width / 2;
x2 = metrics.width / 2;
ctx.lineWidth = Math.max(Math.floor(elem._size / 15), 1);
ctx.strokeStyle = ctx.fillStyle;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
ctx.restore();
},
getBoundingClientRect: function (elem, rect) {
const ctx = webgl.ctx;
ctx.font = [
elem._style,
elem._weight,
elem._size + 'px/' + elem._leading + 'px',
elem._family,
].join(' ');
ctx.textAlign = 'center';
ctx.textBaseline =
CanvasRenderer.Utils.baselines[elem._baseline] || elem._baseline;
const metrics = ctx.measureText(elem._value);
let width = metrics.width;
// TODO: Why does the height need to be scaled by 15%
// in order to not cut off / mask the bitmap data.
let height =
1.15 *
(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent);
if (this._linewidth && !webgl.isHidden.test(this._stroke)) {
width += this._linewidth * 2;
height += this._linewidth * 2;
}
const w = width / 2;
const h = height / 2;
switch (webgl.alignments[elem._alignment] || elem._alignment) {
case webgl.alignments.left:
if (elem.direction === 'ltr') {
rect.left = 0;
rect.right = width;
} else {
rect.left = -width;
rect.right = 0;
}
break;
case webgl.alignments.right:
if (elem.direction === 'ltr') {
rect.left = -width;
rect.right = 0;
} else {
rect.left = 0;
rect.right = width;
}
break;
default:
rect.left = -w;
rect.right = w;
}
// TODO: Gradients aren't inherited...
switch (elem._baseline) {
case 'bottom':
rect.top = -height;
rect.bottom = 0;
break;
case 'top':
rect.top = 0;
rect.bottom = height;
break;
case 'baseline':
rect.top = -h * 1.5; // TODO: Improve calculation based on text metrics
rect.bottom = h * 0.5;
break;
default:
rect.top = -h;
rect.bottom = h;
}
rect.width = width;
rect.height = height;
if (!rect.centroid) {
rect.centroid = {};
}
// TODO:
rect.centroid.x = w;
rect.centroid.y = h;
},
render: function (gl, programs, forcedParent) {
if (!this._visible || !this._opacity) {
return this;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
// Calculate what changed
const parent = forcedParent || this.parent;
const program = programs[this._renderer.type];
const flagParentMatrix = parent._matrix.manual || parent._flagMatrix;
const flagMatrix = this._matrix.manual || this._flagMatrix;
const parentChanged = this._renderer.parent !== parent;
const flagTexture =
this._flagVertices ||
this._flagFill ||
(this._fill instanceof LinearGradient &&
(this._fill._flagSpread ||
this._fill._flagStops ||
this._fill._flagEndPoints)) ||
(this._fill instanceof RadialGradient &&
(this._fill._flagSpread ||
this._fill._flagStops ||
this._fill._flagRadius ||
this._fill._flagCenter ||
this._fill._flagFocal)) ||
(this._fill instanceof Texture &&
((this._fill._flagLoaded && this._fill.loaded) ||
this._fill._flagImage ||
this._fill._flagVideo ||
this._fill._flagRepeat ||
this._fill._flagOffset ||
this._fill._flagScale)) ||
(this._stroke instanceof LinearGradient &&
(this._stroke._flagSpread ||
this._stroke._flagStops ||
this._stroke._flagEndPoints)) ||
(this._stroke instanceof RadialGradient &&
(this._stroke._flagSpread ||
this._stroke._flagStops ||
this._stroke._flagRadius ||
this._stroke._flagCenter ||
this._stroke._flagFocal)) ||
(this._stroke instanceof Texture &&
((this._stroke._flagLoaded && this._stroke.loaded) ||
this._stroke._flagImage ||
this._stroke._flagVideo ||
this._stroke._flagRepeat ||
this._stroke._flagOffset ||
this._fill._flagScale)) ||
this._flagStroke ||
this._flagLinewidth ||
this._flagOpacity ||
parent._flagOpacity ||
this._flagVisible ||
this._flagScale ||
this._flagValue ||
this._flagFamily ||
this._flagSize ||
this._flagLeading ||
this._flagAlignment ||
this._flagBaseline ||
this._flagStyle ||
this._flagWeight ||
this._flagDecoration ||
(this.dashes && this.dashes.length > 0) ||
!this._renderer.texture;
if (flagParentMatrix || flagMatrix || parentChanged) {
if (!this._renderer.matrix) {
this._renderer.matrix = new NumArray(9);
}
// Reduce amount of object / array creation / deletion
this._matrix.toTransformArray(true, transformation);
multiplyMatrix(
transformation,
parent._renderer.matrix,
this._renderer.matrix,
);
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
let sx, sy;
if (this._scale instanceof Vector) {
sx = this._scale.x * parent._renderer.scale.x;
sy = this._scale.y * parent._renderer.scale.y;
} else {
sx = this._scale * parent._renderer.scale.x;
sy = this._scale * parent._renderer.scale.y;
}
this._renderer.scale.x = sx < 0 ? -sx : sx;
this._renderer.scale.y = sy < 0 ? -sy : sy;
if (parentChanged) {
this._renderer.parent = parent;
}
}
if (this._mask) {
// Stencil away everything that isn't rendered by the mask
gl.clear(gl.STENCIL_BUFFER_BIT);
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.ALWAYS, 1, 0);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
// Don't draw the element onto the canvas, only onto the stencil buffer
gl.colorMask(false, false, false, false);
const prop = CanvasRenderer.Utils.getRendererType(
this._mask._renderer.type,
);
webgl[prop].render.call(this._mask, gl, programs, this);
gl.stencilFunc(gl.EQUAL, 1, 0xff);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
gl.colorMask(true, true, true, true);
}
if (flagTexture) {
if (!this._renderer.rect) {
this._renderer.rect = {};
}
this._renderer.opacity = this._opacity * parent._renderer.opacity;
webgl.text.getBoundingClientRect(this, this._renderer.rect);
webgl.updateTexture.call(webgl, gl, this);
} else {
// We still need to update child Two elements on the fill and
// stroke properties.
if (this._fill && this._fill._update) {
this._fill._update();
}
if (this._stroke && this._stroke._update) {
this._stroke._update();
}
}
if ((this._clip && !forcedParent) || !this._renderer.texture) {
return this;
}
if (programs.current !== program) {
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, programs.buffers.position);
gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.position);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
if (!programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, 'u_resolution'),
programs.resolution.width,
programs.resolution.height,
);
}
programs.current = program;
}
if (programs.resolution.flagged) {
gl.uniform2f(
gl.getUniformLocation(program, 'u_resolution'),
programs.resolution.width,
programs.resolution.height,
);
}
// Draw Texture
gl.bindTexture(gl.TEXTURE_2D, this._renderer.texture);
// Draw Rect
const rect = this._renderer.rect;
gl.uniformMatrix3fv(program.matrix, false, this._renderer.matrix);
gl.uniform4f(program.rect, rect.left, rect.top, rect.right, rect.bottom);
gl.drawArrays(gl.TRIANGLES, 0, 6);
if (this._mask) {
gl.disable(gl.STENCIL_TEST);
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
'linear-gradient': {
render: function (ctx, parent) {
if (!ctx.canvas.getContext('2d') || !parent) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
if (
!this._renderer.effect ||
this._flagEndPoints ||
this._flagStops ||
this._flagUnits
) {
let rect;
let lx = this.left._x;
let ly = this.left._y;
let rx = this.right._x;
let ry = this.right._y;
if (/objectBoundingBox/i.test(this._units)) {
// Convert objectBoundingBox units to userSpaceOnUse units
rect = parent.getBoundingClientRect(true);
lx = (lx - 0.5) * rect.width;
ly = (ly - 0.5) * rect.height;
rx = (rx - 0.5) * rect.width;
ry = (ry - 0.5) * rect.height;
}
this._renderer.effect = ctx.createLinearGradient(lx, ly, rx, ry);
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
this._renderer.effect.addColorStop(stop._offset, stop._color);
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
'radial-gradient': {
render: function (ctx, parent) {
if (!ctx.canvas.getContext('2d') || !parent) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
if (
!this._renderer.effect ||
this._flagCenter ||
this._flagFocal ||
this._flagRadius ||
this._flagStops ||
this._flagUnits
) {
let rect;
let cx = this.center._x;
let cy = this.center._y;
let fx = this.focal._x;
let fy = this.focal._y;
let radius = this._radius;
if (/objectBoundingBox/i.test(this._units)) {
// Convert objectBoundingBox units to userSpaceOnUse units
rect = parent.getBoundingClientRect(true);
cx = (cx - 0.5) * rect.width * 0.5;
cy = (cy - 0.5) * rect.height * 0.5;
fx = (fx - 0.5) * rect.width * 0.5;
fy = (fy - 0.5) * rect.height * 0.5;
radius *= Math.min(rect.width, rect.height);
}
this._renderer.effect = ctx.createRadialGradient(
cx,
cy,
0,
fx,
fy,
radius,
);
for (let i = 0; i < this.stops.length; i++) {
const stop = this.stops[i];
this._renderer.effect.addColorStop(stop._offset, stop._color);
}
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
texture: {
render: function (ctx, elem) {
if (!ctx.canvas.getContext('2d')) {
return;
}
if (_.isFunction(this._renderer.onBeforeRender)) {
this._renderer.onBeforeRender();
}
this._update();
const image = this.image;
if (
(this._flagLoaded ||
this._flagImage ||
this._flagVideo ||
this._flagRepeat) &&
this.loaded
) {
this._renderer.effect = ctx.createPattern(image, this._repeat);
} else if (!this._renderer.effect) {
return this.flagReset();
}
if (this._flagOffset || this._flagLoaded || this._flagScale) {
if (!(this._renderer.offset instanceof Vector)) {
this._renderer.offset = new Vector();
}
this._renderer.offset.x = -this._offset.x;
this._renderer.offset.y = -this._offset.y;
if (image) {
this._renderer.offset.x += image.width / 2;
this._renderer.offset.y += image.height / 2;
if (this._scale instanceof Vector) {
this._renderer.offset.x *= this._scale.x;
this._renderer.offset.y *= this._scale.y;
} else {
this._renderer.offset.x *= this._scale;
this._renderer.offset.y *= this._scale;
}
}
}
if (this._flagScale || this._flagLoaded) {
if (!(this._renderer.scale instanceof Vector)) {
this._renderer.scale = new Vector();
}
let sx, sy;
if (this._scale instanceof Vector) {
sx = this._scale.x;
sy = this._scale.y;
} else {
sx = this._scale;
sy = this._scale;
}
this._renderer.scale.x = sx < 0 ? -sx : sx;
this._renderer.scale.y = sy < 0 ? -sy : sy;
}
if (_.isFunction(this._renderer.onAfterRender)) {
this._renderer.onAfterRender();
}
return this.flagReset();
},
},
updateTexture: function (gl, elem) {
const prop = CanvasRenderer.Utils.getRendererType(elem._renderer.type);
this[prop].updateCanvas.call(webgl, gl, elem);
if (this.canvas.width <= 0 || this.canvas.height <= 0) {
if (elem._renderer.texture) {
gl.deleteTexture(elem._renderer.texture);
}
delete elem._renderer.texture;
return;
}
if (!elem._renderer.texture) {
elem._renderer.texture = gl.createTexture();
}
gl.bindTexture(gl.TEXTURE_2D, elem._renderer.texture);
// Upload the image into the texture.
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
this.canvas,
);
// Set the parameters so we can render any size image.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// if ('EXT_texture_filter_anisotropic' in gl.extensions) {
// const e = gl.extensions.EXT_texture_filter_anisotropic;
// const maxAnisotropy = gl.getParameter(e.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
// gl.texParameterf(gl.TEXTURE_2D, e.TEXTURE_MAX_ANISOTROPY_EXT, maxAnisotropy);
// }
},
program: {
create: function (gl, shaders) {
let program, linked, error;
program = gl.createProgram();
_.each(shaders, function (s) {
gl.attachShader(program, s);
});
gl.linkProgram(program);
linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
error = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw new TwoError('unable to link program: ' + error);
}
return program;
},
},
extensions: {
init: function (gl) {
const extensions = {};
const names = [
'EXT_texture_filter_anisotropic',
'WEBGL_compressed_texture_s3tc',
'OES_texture_float_linear',
'WEBGL_multisampled_render_to_texture',
];
for (let i = 0; i < names.length; i++) {
const name = names[i];
extensions[name] = webgl.extensions.get(gl, name);
}
return extensions;
},
get: function (gl, name) {
return (
gl.getExtension(name) ||
gl.getExtension(`MOZ_${name}`) ||
gl.getExtension(`WEBKIT_${name}`)
);
},
},
TextureRegistry: new Registry(),
};
webgl.ctx = webgl.canvas.getContext('2d');
/**
* @name Two.WebGLRenderer
* @class
* @extends Two.Events
* @param {Object} [parameters] - This object is inherited when constructing a new instance of {@link Two}.
* @param {Element} [parameters.domElement] - The `` to draw to. If none given a new one will be constructed.
* @param {HTMLCanvasElement} [parameters.offscreenElement] - The offscreen two dimensional `` to render each element on WebGL texture updates.
* @param {Boolean} [parameters.antialias] - Determines whether the canvas should clear render with antialias on.
* @description This class is used by {@link Two} when constructing with `type` of `Two.Types.webgl`. It takes Two.js' scenegraph and renders it to a `` through the WebGL api.
* @see {@link https://www.khronos.org/registry/webgl/specs/latest/1.0/}
*/
export class Renderer extends Events {
constructor(params) {
super();
let gl, program, vs, fs;
/**
* @name Two.WebGLRenderer#domElement
* @property {Element} - The `` associated with the Two.js scene.
*/
this.domElement = params.domElement || document.createElement('canvas');
if (typeof params.offscreenElement !== 'undefined') {
webgl.canvas = params.offscreenElement;
webgl.ctx = webgl.canvas.getContext('2d');
}
/**
* @name Two.WebGLRenderer#scene
* @property {Two.Group} - The root group of the scenegraph.
*/
this.scene = new Group();
this.scene.parent = this;
this._renderer = {
type: 'renderer',
matrix: new NumArray(identity),
scale: 1,
opacity: 1,
};
this._flagMatrix = true;
// http://games.greggman.com/game/webgl-and-alpha/
// http://www.khronos.org/registry/webgl/specs/latest/#5.2
params = _.defaults(params || {}, {
antialias: false,
alpha: true,
premultipliedAlpha: true,
stencil: true,
preserveDrawingBuffer: true,
overdraw: false,
});
/**
* @name Two.WebGLRenderer#overdraw
* @property {Boolean} - Determines whether the canvas clears the background each draw call.
* @default true
*/
this.overdraw = params.overdraw;
/**
* @name Two.WebGLRenderer#ctx
* @property {WebGLContext} - Associated two dimensional context to render on the ``.
*/
gl = this.ctx =
this.domElement.getContext('webgl', params) ||
this.domElement.getContext('experimental-webgl', params);
if (!this.ctx) {
throw new TwoError(
'unable to create a webgl context. Try using another renderer.',
);
}
// Compile Base Shaders to draw in pixel space.
vs = shaders.create(gl, shaders.path.vertex, shaders.types.vertex);
fs = shaders.create(gl, shaders.path.fragment, shaders.types.fragment);
/**
* @name Two.WebGLRenderer#programs
* @property {Object} - Associated WebGL programs to render all elements from the scenegraph.
*/
this.programs = {
current: null,
buffers: {
position: gl.createBuffer(),
},
resolution: {
width: 0,
height: 0,
ratio: 1,
flagged: false,
},
};
program = this.programs.path = webgl.program.create(gl, [vs, fs]);
this.programs.text = this.programs.path;
gl.extensions = webgl.extensions.init(gl);
gl.renderer = this;
// Create and bind the drawing buffer
// look up where the vertex data needs to go.
program.position = gl.getAttribLocation(program, 'a_position');
program.matrix = gl.getUniformLocation(program, 'u_matrix');
program.rect = gl.getUniformLocation(program, 'u_rect');
// Bind the vertex buffer
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.position);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
// Compile Base Shaders to draw in pixel space.
vs = shaders.create(gl, shaders.points.vertex, shaders.types.vertex);
fs = shaders.create(gl, shaders.points.fragment, shaders.types.fragment);
program = this.programs.points = webgl.program.create(gl, [vs, fs]);
// Create and bind the drawing buffer
// look up where the vertex data needs to go.
program.position = gl.getAttribLocation(program, 'a_position');
program.matrix = gl.getUniformLocation(program, 'u_matrix');
program.size = gl.getUniformLocation(program, 'u_size');
// Setup some initial statements of the gl context
gl.enable(gl.BLEND);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
}
/**
* @name Two.WebGLRenderer.Utils
* @property {Object} - A massive object filled with utility functions and properties to render Two.js objects to a `` through the WebGL API.
*/
static Utils = webgl;
/**
* @name Two.WebGLRenderer#setSize
* @function
* @fires resize
* @param {Number} width - The new width of the renderer.
* @param {Number} height - The new height of the renderer.
* @param {Number} [ratio] - The new pixel ratio (pixel density) of the renderer. Defaults to calculate the pixel density of the user's screen.
* @description Change the size of the renderer.
*/
setSize(width, height, ratio) {
let w, h;
const ctx = this.ctx;
this.width = width;
this.height = height;
this.ratio = typeof ratio === 'undefined' ? getRatio(ctx) : ratio;
this.domElement.width = width * this.ratio;
this.domElement.height = height * this.ratio;
if (_.isObject(this.domElement.style)) {
_.extend(this.domElement.style, {
width: width + 'px',
height: height + 'px',
});
}
// Set for this.stage parent scaling to account for HDPI
this._renderer.matrix[0] =
this._renderer.matrix[4] =
this._renderer.scale =
this.ratio;
this._flagMatrix = true;
w = width * this.ratio;
h = height * this.ratio;
ctx.viewport(0, 0, w, h);
this.programs.resolution.width = w;
this.programs.resolution.height = h;
this.programs.resolution.ratio = this.ratio;
this.programs.resolution.flagged = true;
return this.trigger(Events.Types.resize, width, height, ratio);
}
/**
* @name Two.WebGLRenderer#render
* @function
* @description Render the current scene to the ``.
*/
render() {
const gl = this.ctx;
if (!this.overdraw) {
gl.clear(gl.COLOR_BUFFER_BIT);
}
webgl.group.render.call(this.scene, gl, this.programs);
this._flagMatrix = false;
this.programs.resolution.flagged = true;
return this;
}
}
================================================
FILE: src/shape.d.ts
================================================
declare module 'two.js/src/shape' {
export interface ShapeHitTestOptions {
precision?: number;
tolerance?: number;
fill?: boolean;
stroke?: boolean;
ignoreVisibility?: boolean;
}
/**
* @name Two.Shape
* @class
* @description The foundational transformation object for the Two.js scenegraph.
*/
export class Shape extends TwoElement {
static Properties: (
| 'position'
| 'rotation'
| 'scale'
| 'skewX'
| 'skewY'
| 'matrix'
| 'worldMatrix'
| string
)[];
/**
* @name Two.Shape.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Shape} to create a new instance
* @returns {Two.Shape}
* @description Create a new {@link Two.Shape} from an object notation of a {@link Two.Shape}.
* @nota-bene Works in conjunction with {@link Two.Shape#toObject}
*/
static fromObject(
obj: Parameters[0] &
((
| {
translation?: { x: number; y: number } | Vector;
position?: never;
}
| {
position?: { x: number; y: number } | Vector;
translation?: never;
}
) & {
rotation?: number;
scale?: number | { x: number; y: number } | Vector;
skewX?: number;
skewY?: number;
matrix?: Parameters[0];
})
): Shape;
/**
* @name Two.Shape#_flagMatrix
* @private
* @property {Boolean} - Determines whether the matrix needs updating.
*/
private _flagMatrix;
/**
* @name Two.Shape#_flagScale
* @private
* @property {Boolean} - Determines whether the scale needs updating.
*/
private _flagScale;
/**
* @name Two.Shape#_matrix
* @private
* @property {Matrix} - The matrix value of the shape's position, rotation, and scale.
*/
private _matrix;
/**
* @name Two.Shape#_worldMatrix
* @private
* @property {Matrix} - The matrix value of the shape's position, rotation, and scale in the scene.
*/
private _worldMatrix;
/**
* @name Two.Shape#_position
* @private
* @property {Vector} - The translation values as a {@link Two.Vector}.
*/
private _position;
/**
* @name Two.Shape#_rotation
* @private
* @property {Number} - The rotation value in Number.
*/
private _rotation;
/**
* @name Two.Shape#_scale
* @private
* @property {Number|Vector} - The scale value in Number. Can be a vector for non-uniform scaling.
*/
private _scale;
/**
* @name Two.Shape#_skewX
* @private
* @property {Number} - The rotation value in Number.
*/
private _skewX;
/**
* @name Two.Shape#_skewY
* @private
* @property {Number} - The rotation value in Number.
*/
private _skewY;
isShape: true;
/**
* @name Two.Shape#id
* @property {String} - Session specific unique identifier.
* @nota-bene In the {@link Two.SVGRenderer} change this to change the underlying SVG element's id too.
*/
id: string;
/**
* @name Two.Shape#matrix
* @property {Matrix}
* @description The transformation matrix of the shape.
* @nota-bene {@link Two.Shape#position}, {@link Two.Shape#rotation}, {@link Two.Shape#scale}, {@link Two.Shape#skewX}, and {@link Two.Shape#skewY} apply their values to the matrix when changed. The matrix is what is sent to the renderer to be drawn.
*/
matrix: Matrix;
/**
* @name Two.Shape#worldMatrix
* @property {Matrix}
* @description The transformation matrix of the shape in the scene.
*/
worldMatrix: Matrix;
/**
* @name Two.Shape#position
* @property {Vector} - The x and y value for where the shape is placed relative to its parent.
*/
position: Vector;
/**
* @name Two.Shape#rotation
* @property {Number} - The value in Number for how much the shape is rotated relative to its parent.
*/
rotation: number;
/**
* @name Two.Shape#scale
* @property {Number} - The value for how much the shape is scaled relative to its parent.
* @nota-bene This value can be replaced with a {@link Two.Vector} to do non-uniform scaling. e.g: `shape.scale = new Two.Vector(2, 1);`
*/
scale: number | Vector;
/**
* @name Two.Shape#skewX
* @property {Number} - The value in Number for how much the shape is skewed relative to its parent.
* @description Skew the shape by an angle in the x axis direction.
*/
skewX: number;
/**
* @name Two.Shape#skewY
* @property {Number} - The value in Number for how much the shape is skewed relative to its parent.
* @description Skew the shape by an angle in the y axis direction.
*/
skewY: number;
set translation(arg: Vector);
/**
* @name Two.Shape#translation
* @description Alias for {@link Two.Shape#position}.
*/
get translation(): Vector;
/**
* @name Two.Shape#addTo
* @function
* @param {Group} group - The parent the shape adds itself to.
* @description Convenience method to add itself to the scenegraph.
*/
addTo(group: Group): Shape;
/**
* @name Two.Shape#remove
* @function
* @description Remove self from the scene / parent.
*/
remove(): Shape;
/**
* @name Two.Shape#copy
* @function
* @param {Two.Shape} shape
* @description Copy the properties of one {@link Two.Shape} onto another.
*/
copy(shape: Shape): Shape;
/**
* @name Two.Shape#clone
* @function
* @param {Group} [parent] - Optional argument to automatically add the shape to a scenegraph.
* @returns {Shape}
* @description Create a new {@link Two.Shape} with the same values as the current shape.
*/
clone(parent?: Group): Shape;
/**
* @name Two.Shape#toObject
* @function
* @description Create a JSON compatible object that represents information of the shape.
* @nota-bene Works in conjunction with {@link Two.Shape.fromObject}
*/
toObject(): object;
/**
* @name Two.Shape#dispose
* @function
* @description Release the shape's bound objects by unbinding relevant events.
*/
dispose(): Shape;
/**
* @name Two.Shape#contains
* @function
* @param {Number} x - x coordinate to hit test against
* @param {Number} y - y coordinate to hit test against
* @param {Object} [options] - Optional options object
* @param {Boolean} [options.ignoreVisibility] - If `true`, hit test against `shape.visible = false` shapes
* @param {Number} [options.tolerance] - Padding to hit test against in pixels
* @description Check to see if coordinates are within a {@link Two.Shape}'s bounding rectangle
*/
contains(x: number, y: number, options?: ShapeHitTestOptions): boolean;
/**
* @name Two.Shape#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
protected _update(bubbles?: boolean): Shape;
/**
* @name Two.Shape#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset(): Shape;
}
import { Element as TwoElement } from 'two.js/src/element';
import { Matrix } from 'two.js/src/matrix';
import { Vector } from 'two.js/src/vector';
import { Group } from 'two.js/src/group';
}
================================================
FILE: src/shape.js
================================================
import { Events } from './events.js';
import { Element } from './element.js';
import { Matrix } from './matrix.js';
import { Vector } from './vector.js';
import { getComputedMatrix } from './utils/math.js';
/**
* @name Two.Shape
* @class
* @extends Two.Element
* @description The foundational transformation object for the Two.js scenegraph.
*/
export class Shape extends Element {
/**
* @name Two.Shape#_flagMatrix
* @private
* @property {Boolean} - Determines whether the matrix needs updating.
*/
_flagMatrix = true;
/**
* @name Two.Shape#_flagScale
* @private
* @property {Boolean} - Determines whether the scale needs updating.
*/
_flagScale = false;
// Underlying Properties
/**
* @name Two.Shape#_matrix
* @private
* @property {Two.Matrix} - The matrix value of the shape's position, rotation, and scale.
*/
_matrix = null;
/**
* @name Two.Shape#_worldMatrix
* @private
* @property {Two.Matrix} - The matrix value of the shape's position, rotation, and scale in the scene.
*/
_worldMatrix = null;
/**
* @name Two.Shape#_position
* @private
* @property {Two.Vector} - The translation values as a {@link Two.Vector}.
*/
_position = null;
/**
* @name Two.Shape#_rotation
* @private
* @property {Number} - The rotation value in radians.
*/
_rotation = 0;
/**
* @name Two.Shape#_scale
* @private
* @property {Number|Two.Vector} - The scale value in Number. Can be a vector for non-uniform scaling.
*/
_scale = 1;
/**
* @name Two.Shape#_skewX
* @private
* @property {Number} - The rotation value in Number.
*/
_skewX = 0;
/**
* @name Two.Shape#_skewY
* @private
* @property {Number} - The rotation value in Number.
*/
_skewY = 0;
constructor() {
super();
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
/**
* @name Two.Shape#renderer
* @property {Object}
* @description Object access to store relevant renderer specific variables. Warning: manipulating this object can create unintended consequences.
* @nota-bene With the {@link Two.SVGRenderer} you can access the underlying SVG element created via `shape.renderer.elem`.
*/
this._renderer.flagMatrix = FlagMatrix.bind(this);
this.isShape = true;
/**
* @name Two.Shape#matrix
* @property {Two.Matrix}
* @description The transformation matrix of the shape.
* @nota-bene {@link Two.Shape#position}, {@link Two.Shape#rotation}, {@link Two.Shape#scale}, {@link Two.Shape#skewX}, and {@link Two.Shape#skewY} apply their values to the matrix when changed. The matrix is what is sent to the renderer to be drawn.
*/
this.matrix = new Matrix();
/**
* @name Two.Shape#worldMatrix
* @property {Two.Matrix}
* @description The transformation matrix of the shape in the scene.
*/
this.worldMatrix = new Matrix();
/**
* @name Two.Shape#position
* @property {Two.Vector} - The x and y value for where the shape is placed relative to its parent.
*/
this.position = new Vector();
/**
* @name Two.Shape#rotation
* @property {Number} - The value in Number for how much the shape is rotated relative to its parent.
*/
this.rotation = 0;
/**
* @name Two.Shape#scale
* @property {Number} - The value for how much the shape is scaled relative to its parent.
* @nota-bene This value can be replaced with a {@link Two.Vector} to do non-uniform scaling. e.g: `shape.scale = new Two.Vector(2, 1);`
*/
this.scale = 1;
/**
* @name Two.Shape#skewX
* @property {Number} - The value in Number for how much the shape is skewed relative to its parent.
* @description Skew the shape by an angle in the x axis direction.
*/
this.skewX = 0;
/**
* @name Two.Shape#skewY
* @property {Number} - The value in Number for how much the shape is skewed relative to its parent.
* @description Skew the shape by an angle in the y axis direction.
*/
this.skewY = 0;
}
static Properties = [
'position',
'rotation',
'scale',
'skewX',
'skewY',
'matrix',
'worldMatrix',
];
/**
* @name Two.Shape.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Shape} to create a new instance
* @returns {Two.Shape}
* @description Create a new {@link Two.Shape} from an object notation of a {@link Two.Shape}.
* @nota-bene Works in conjunction with {@link Two.Shape#toObject}
*/
static fromObject(obj) {
const shape = new Shape().copy(obj);
if ('id' in obj) {
shape.id = obj.id;
}
return shape;
}
get renderer() {
return this._renderer;
}
set renderer(v) {
this._renderer = v;
}
/**
* @name Two.Shape#translation
* @description Alias for {@link Two.Shape#position}.
*/
get translation() {
return proto.position.get.apply(this, arguments);
}
set translation(v) {
proto.position.set.apply(this, arguments);
}
/**
* @name Two.Shape#addTo
* @function
* @param {Two.Group} group - The parent the shape adds itself to.
* @description Convenience method to add itself to the scenegraph.
*/
addTo(group) {
group.add(this);
return this;
}
/**
* @name Two.Shape#remove
* @function
* @description Remove self from the scene / parent.
*/
remove() {
if (!this.parent) {
return this;
}
this.parent.remove(this);
return this;
}
/**
* @name Two.Shape#contains
* @function
* @param {Number} x - x coordinate to hit test against
* @param {Number} y - y coordinate to hit test against
* @param {Object} [options] - Optional options object
* @param {Boolean} [options.ignoreVisibility] - If `true`, hit test against `shape.visible = false` shapes
* @param {Number} [options.tolerance] - Padding to hit test against in pixels
* @returns {Boolean}
* @description Check to see if coordinates are within a {@link Two.Shape}'s bounding rectangle
* @nota-bene Expects *world-space coordinates* – the same pixel-space you get from the renderer (e.g., mouse `clientX`/`clientY` adjusted for the canvas’s offset and pixel ratio).
*/
contains(x, y, options) {
const opts = options || {};
const ignoreVisibility = opts.ignoreVisibility === true;
if (!ignoreVisibility && 'visible' in this && this.visible === false) {
return false;
}
if (
!ignoreVisibility &&
'opacity' in this &&
typeof this.opacity === 'number' &&
this.opacity <= 0
) {
return false;
}
if (typeof this.getBoundingClientRect !== 'function') {
return false;
}
const tolerance = typeof opts.tolerance === 'number' ? opts.tolerance : 0;
this._update(true);
const rect = this.getBoundingClientRect();
if (!rect) {
return false;
}
return (
x >= rect.left - tolerance &&
x <= rect.right + tolerance &&
y >= rect.top - tolerance &&
y <= rect.bottom + tolerance
);
}
/**
* @name Two.Shape#copy
* @function
* @param {Two.Shape} shape
* @description Copy the properties of one {@link Two.Shape} onto another.
*/
copy(shape) {
super.copy.call(this, shape);
if ('position' in shape) {
if (shape.position instanceof Vector) {
this.position = shape.position;
} else {
this.position.copy(shape.position);
}
}
if ('rotation' in shape) {
this.rotation = shape.rotation;
}
if ('scale' in shape) {
this.scale =
typeof shape.scale === 'number' || shape.scale instanceof Vector
? shape.scale
: new Vector(shape.scale.x, shape.scale.y);
}
if ('skewX' in shape) {
this.skewX = shape.skewX;
}
if ('skewY' in shape) {
this.skewY = shape.skewY;
}
if ('matrix' in shape && shape.matrix.manual) {
this.matrix.copy(shape.matrix);
this.matrix.manual = true;
}
return this;
}
/**
* @name Two.Shape#clone
* @function
* @param {Two.Group} [parent] - Optional argument to automatically add the shape to a scenegraph.
* @returns {Two.Shape}
* @description Create a new {@link Two.Shape} with the same values as the current shape.
*/
clone(parent) {
const clone = new Shape();
clone.position.copy(this.position);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
if (parent) {
parent.add(clone);
}
return clone._update();
}
/**
* @name Two.Shape#toObject
* @function
* @description Create a JSON compatible object that represents information of the shape.
* @nota-bene Works in conjunction with {@link Two.Shape.fromObject}
*/
toObject() {
const result = super.toObject.call(this);
result.renderer = { type: 'shape' };
result.isShape = true;
result.translation = this.translation.toObject();
result.rotation = this.translation.rotation;
result.scale =
this.scale instanceof Vector ? this.scale.toObject() : this.scale;
result.skewX = this.skewX;
result.skewY = this.skewY;
result.matrix = this.matrix.toObject();
return result;
}
/**
* @name Two.Shape#dispose
* @function
* @description Release the shape's bound objects by unbinding relevant events.
*/
dispose() {
// Call parent dispose to preserve renderer type and unbind events
super.dispose();
if (
typeof this.translation === 'object' &&
typeof this.translation.unbind === 'function'
) {
this.translation.unbind();
}
if (
typeof this.scale === 'object' &&
typeof this.scale.unbind === 'function'
) {
this.scale.unbind();
}
}
/**
* @name Two.Shape#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update(bubbles) {
if (!this._matrix.manual && this._flagMatrix) {
this._matrix.identity().translate(this.position.x, this.position.y);
this._matrix.rotate(this.rotation);
if (this._scale instanceof Vector) {
this._matrix.scale(this._scale.x, this._scale.y);
} else {
this._matrix.scale(this._scale);
}
this._matrix.skewX(this.skewX);
this._matrix.skewY(this.skewY);
}
if (bubbles) {
if (this.parent && this.parent._update) {
this.parent._update();
}
}
return this;
}
/**
* @name Two.Shape#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagMatrix = this._flagScale = false;
super.flagReset.call(this);
return this;
}
}
const proto = {
position: {
enumerable: true,
get: function () {
return this._position;
},
set: function (v) {
if (this._position) {
this._position.unbind(Events.Types.change, this._renderer.flagMatrix);
}
this._position = v;
this._position.bind(Events.Types.change, this._renderer.flagMatrix);
FlagMatrix.call(this);
},
},
rotation: {
enumerable: true,
get: function () {
return this._rotation;
},
set: function (v) {
this._rotation = v;
this._flagMatrix = true;
},
},
scale: {
enumerable: true,
get: function () {
return this._scale;
},
set: function (v) {
if (this._scale instanceof Vector) {
this._scale.unbind(Events.Types.change, this._renderer.flagMatrix);
}
this._scale = v;
if (this._scale instanceof Vector) {
this._scale.bind(Events.Types.change, this._renderer.flagMatrix);
}
this._flagMatrix = true;
this._flagScale = true;
},
},
skewX: {
enumerable: true,
get: function () {
return this._skewX;
},
set: function (v) {
this._skewX = v;
this._flagMatrix = true;
},
},
skewY: {
enumerable: true,
get: function () {
return this._skewY;
},
set: function (v) {
this._skewY = v;
this._flagMatrix = true;
},
},
matrix: {
enumerable: true,
get: function () {
return this._matrix;
},
set: function (v) {
this._matrix = v;
this._flagMatrix = true;
},
},
worldMatrix: {
enumerable: true,
get: function () {
// TODO: Make DRY
getComputedMatrix(this, this._worldMatrix);
return this._worldMatrix;
},
set: function (v) {
this._worldMatrix = v;
},
},
};
/**
* @name FlagMatrix
* @function
* @private
* @description Utility function used in conjunction with event handlers to update the flagMatrix of a shape.
*/
function FlagMatrix() {
this._flagMatrix = true;
}
================================================
FILE: src/shapes/arc-segment.d.ts
================================================
declare module 'two.js/src/shapes/arc-segment' {
/**
* @name Two.ArcSegment
* @class
* @param {Number} [x=0] - The x position of the arc segment.
* @param {Number} [y=0] - The y position of the arc segment.
* @param {Number} [innerRadius=0] - The inner radius value of the arc segment.
* @param {Number} [outerRadius=0] - The outer radius value of the arc segment.
* @param {Number} [startAngle=0] - The start angle of the arc segment in Number.
* @param {Number} [endAngle=6.2831] - The end angle of the arc segment in Number.
* @param {Number} [resolution=24] - The number of vertices used to construct the arc segment.
*/
export class ArcSegment extends Path {
static Properties: (
| 'startAngle'
| 'endAngle'
| 'innerRadius'
| 'outerRadius'
| string
)[];
/**
* @name Two.ArcSegment.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.ArcSegment} to create a new instance
* @returns {Two.ArcSegment}
* @description Create a new {@link Two.ArcSegment} from an object notation of a {@link Two.ArcSegment}.
* @nota-bene Works in conjunction with {@link Two.ArcSegment#toObject}
*/
fromObject(
obj: Parameters[0] & {
startAngle?: number;
endAngle?: number;
innerRadius?: number;
outerRadius?: number;
}
): ArcSegment;
constructor(
ox?: number,
oy?: number,
ir?: number,
or?: number,
sa?: number,
ea?: number,
res?: number
);
/**
* @name Two.ArcSegment#_flagStartAngle
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#startAngle} needs updating.
*/
private _flagStartAngle;
/**
* @name Two.ArcSegment#_flagEndAngle
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#endAngle} needs updating.
*/
private _flagEndAngle;
/**
* @name Two.ArcSegment#_flagInnerRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#innerRadius} needs updating.
*/
private _flagInnerRadius;
/**
* @name Two.ArcSegment#_flagOuterRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#outerRadius} needs updating.
*/
private _flagOuterRadius;
/**
* @name Two.ArcSegment#_startAngle
* @private
* @see {@link Two.ArcSegment#startAngle}
*/
private _startAngle;
/**
* @name Two.ArcSegment#_endAngle
* @private
* @see {@link Two.ArcSegment#endAngle}
*/
private _endAngle;
/**
* @name Two.ArcSegment#_innerRadius
* @private
* @see {@link Two.ArcSegment#innerRadius}
*/
private _innerRadius;
/**
* @name Two.ArcSegment#_outerRadius
* @private
* @see {@link Two.ArcSegment#outerRadius}
*/
private _outerRadius;
/**
* @name Two.ArcSegment#innerRadius
* @property {Number} - The size of the inner radius of the arc segment.
*/
innerRadius: number;
/**
* @name Two.ArcSegment#outerRadius
* @property {Number} - The size of the outer radius of the arc segment.
*/
outerRadius: number;
/**
* @name Two.ArcSegment#startAngle
* @property {Number} - The angle of one side for the arc segment.
*/
startAngle: number;
/**
* @name Two.ArcSegment#endAngle
* @property {Number} - The angle of the other side for the arc segment.
*/
endAngle: number;
/**
* @name Two.ArcSegment#copy
* @function
* @param {Two.ArcSegment} arcSegment - The reference {@link Two.ArcSegment}
* @description Copy the properties of one {@link Two.ArcSegment} onto another.
*/
copy(arcSegment: ArcSegment): ArcSegment;
/**
* @name Two.ArcSegment#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.ArcSegment}
* @description Create a new instance of {@link Two.ArcSegment} with the same properties of the current path.
*/
clone(parent: Group): ArcSegment;
}
import { Path } from 'two.js/src/path';
import { Group } from 'two.js/src/group';
}
================================================
FILE: src/shapes/arc-segment.js
================================================
import { Commands } from '../utils/path-commands.js';
import { mod, HALF_PI, TWO_PI } from '../utils/math.js';
import { Path } from '../path.js';
import { Anchor } from '../anchor.js';
import { Constants } from '../constants.js';
/**
* @name Two.ArcSegment
* @class
* @extends Two.Path
* @param {Number} [x=0] - The x position of the arc segment.
* @param {Number} [y=0] - The y position of the arc segment.
* @param {Number} [innerRadius=0] - The inner radius value of the arc segment.
* @param {Number} [outerRadius=0] - The outer radius value of the arc segment.
* @param {Number} [startAngle=0] - The start angle of the arc segment in Number.
* @param {Number} [endAngle=6.2831] - The end angle of the arc segment in Number.
* @param {Number} [resolution=24] - The number of vertices used to construct the arc segment.
*/
export class ArcSegment extends Path {
/**
* @name Two.ArcSegment#_flagStartAngle
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#startAngle} needs updating.
*/
_flagStartAngle = false;
/**
* @name Two.ArcSegment#_flagEndAngle
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#endAngle} needs updating.
*/
_flagEndAngle = false;
/**
* @name Two.ArcSegment#_flagInnerRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#innerRadius} needs updating.
*/
_flagInnerRadius = false;
/**
* @name Two.ArcSegment#_flagOuterRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.ArcSegment#outerRadius} needs updating.
*/
_flagOuterRadius = false;
/**
* @name Two.ArcSegment#_startAngle
* @private
* @see {@link Two.ArcSegment#startAngle}
*/
_startAngle = 0;
/**
* @name Two.ArcSegment#_endAngle
* @private
* @see {@link Two.ArcSegment#endAngle}
*/
_endAngle = TWO_PI;
/**
* @name Two.ArcSegment#_innerRadius
* @private
* @see {@link Two.ArcSegment#innerRadius}
*/
_innerRadius = 0;
/**
* @name Two.ArcSegment#_outerRadius
* @private
* @see {@link Two.ArcSegment#outerRadius}
*/
_outerRadius = 0;
constructor(x, y, ir, or, sa, ea, res) {
const amount = res || Constants.Resolution * 3;
const points = [];
for (let i = 0; i < amount; i++) {
points.push(new Anchor());
}
super(points, true, false, true);
this._renderer.type = 'arc-segment';
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
/**
* @name Two.ArcSegment#innerRadius
* @property {Number} - The size of the inner radius of the arc segment.
*/
if (typeof ir === 'number') {
this.innerRadius = ir;
}
/**
* @name Two.ArcSegment#outerRadius
* @property {Number} - The size of the outer radius of the arc segment.
*/
if (typeof or === 'number') {
this.outerRadius = or;
}
/**
* @name Two.ArcSegment#startAngle
* @property {Number} - The angle of one side for the arc segment.
*/
if (typeof sa === 'number') {
this.startAngle = sa;
}
/**
* @name Two.ArcSegment#endAngle
* @property {Number} - The angle of the other side for the arc segment.
*/
if (typeof ea === 'number') {
this.endAngle = ea;
}
this._update();
if (typeof x === 'number') {
this.translation.x = x;
}
if (typeof y === 'number') {
this.translation.y = y;
}
}
/**
* @name Two.ArcSegment.Properties
* @property {String[]} - A list of properties that are on every {@link Two.ArcSegment}.
*/
static Properties = ['startAngle', 'endAngle', 'innerRadius', 'outerRadius'];
/**
* @name Two.ArcSegment.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.ArcSegment} to create a new instance
* @returns {Two.ArcSegment}
* @description Create a new {@link Two.ArcSegment} from an object notation of a {@link Two.ArcSegment}.
* @nota-bene Works in conjunction with {@link Two.ArcSegment#toObject}
*/
static fromObject(obj) {
const segment = new ArcSegment().copy(obj);
if ('id' in obj) {
segment.id = obj.id;
}
return segment;
}
/**
* @name Two.ArcSegment#copy
* @function
* @param {Two.ArcSegment} arcSegment - The reference {@link Two.ArcSegment}
* @description Copy the properties of one {@link Two.ArcSegment} onto another.
*/
copy(arcSegment) {
super.copy.call(this, arcSegment);
for (let i = 0; i < ArcSegment.Properties.length; i++) {
const k = ArcSegment.Properties[i];
if (k in arcSegment && typeof arcSegment[k] === 'number') {
this[k] = arcSegment[k];
}
}
return this;
}
/**
* @name Two.ArcSegment#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (
this._flagVertices ||
this._flagStartAngle ||
this._flagEndAngle ||
this._flagInnerRadius ||
this._flagOuterRadius
) {
const sa = this._startAngle;
const ea = this._endAngle;
const ir = this._innerRadius;
const or = this._outerRadius;
const connected = mod(sa, TWO_PI) === mod(ea, TWO_PI);
const punctured = ir > 0;
const vertices = this.vertices;
let length = punctured ? vertices.length / 2 : vertices.length;
let command,
id = 0;
let i, last, pct, v, theta, step, x, y, amp;
if (connected) {
length--;
} else if (!punctured) {
length -= 2;
}
/**
* Outer Circle
*/
for (i = 0, last = length - 1; i < length; i++) {
pct = i / last;
v = vertices[id];
theta = pct * (ea - sa) + sa;
step = (ea - sa) / length;
x = or * Math.cos(theta);
y = or * Math.sin(theta);
switch (i) {
case 0:
command = Commands.move;
break;
default:
command = Commands.curve;
}
v.command = command;
v.x = x;
v.y = y;
v.controls.left.clear();
v.controls.right.clear();
if (v.command === Commands.curve) {
amp = (or * step) / Math.PI;
v.controls.left.x = amp * Math.cos(theta - HALF_PI);
v.controls.left.y = amp * Math.sin(theta - HALF_PI);
v.controls.right.x = amp * Math.cos(theta + HALF_PI);
v.controls.right.y = amp * Math.sin(theta + HALF_PI);
if (i === 1) {
v.controls.left.multiplyScalar(2);
}
if (i === last) {
v.controls.right.multiplyScalar(2);
}
}
id++;
}
if (punctured) {
if (connected) {
vertices[id].command = Commands.close;
id++;
} else {
length--;
last = length - 1;
}
/**
* Inner Circle
*/
for (i = 0; i < length; i++) {
pct = i / last;
v = vertices[id];
theta = (1 - pct) * (ea - sa) + sa;
step = (ea - sa) / length;
x = ir * Math.cos(theta);
y = ir * Math.sin(theta);
command = Commands.curve;
if (i <= 0) {
command = connected ? Commands.move : Commands.line;
}
v.command = command;
v.x = x;
v.y = y;
v.controls.left.clear();
v.controls.right.clear();
if (v.command === Commands.curve) {
amp = (ir * step) / Math.PI;
v.controls.left.x = amp * Math.cos(theta + HALF_PI);
v.controls.left.y = amp * Math.sin(theta + HALF_PI);
v.controls.right.x = amp * Math.cos(theta - HALF_PI);
v.controls.right.y = amp * Math.sin(theta - HALF_PI);
if (i === 1) {
v.controls.left.multiplyScalar(2);
}
if (i === last) {
v.controls.right.multiplyScalar(2);
}
}
id++;
}
// Final Point
vertices[id].copy(vertices[0]);
vertices[id].command = Commands.line;
} else if (!connected) {
vertices[id].command = Commands.line;
vertices[id].x = 0;
vertices[id].y = 0;
id++;
// Final Point
vertices[id].copy(vertices[0]);
vertices[id].command = Commands.line;
}
}
super._update.call(this);
return this;
}
/**
* @name Two.ArcSegment#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
super.flagReset.call(this);
this._flagStartAngle =
this._flagEndAngle =
this._flagInnerRadius =
this._flagOuterRadius =
false;
return this;
}
/**
* @name Two.ArcSegment#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.ArcSegment}
* @description Create a new instance of {@link Two.ArcSegment} with the same properties of the current path.
*/
clone(parent) {
const ir = this.innerRadius;
const or = this.outerRadius;
const sa = this.startAngle;
const ea = this.endAngle;
const resolution = this.vertices.length;
const clone = new ArcSegment(0, 0, ir, or, sa, ea, resolution);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.ArcSegment#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = 'arc-segment';
for (let i = 0; i < ArcSegment.Properties.length; i++) {
const k = ArcSegment.Properties[i];
object[k] = this[k];
}
return object;
}
}
const proto = {
startAngle: {
enumerable: true,
get: function () {
return this._startAngle;
},
set: function (v) {
this._startAngle = v;
this._flagStartAngle = true;
},
},
endAngle: {
enumerable: true,
get: function () {
return this._endAngle;
},
set: function (v) {
this._endAngle = v;
this._flagEndAngle = true;
},
},
innerRadius: {
enumerable: true,
get: function () {
return this._innerRadius;
},
set: function (v) {
this._innerRadius = v;
this._flagInnerRadius = true;
},
},
outerRadius: {
enumerable: true,
get: function () {
return this._outerRadius;
},
set: function (v) {
this._outerRadius = v;
this._flagOuterRadius = true;
},
},
};
================================================
FILE: src/shapes/circle.d.ts
================================================
declare module 'two.js/src/shapes/circle' {
/**
* @name Two.Circle
* @class
* @param {Number} [x=0] - The x position of the circle.
* @param {Number} [y=0] - The y position of the circle.
* @param {Number} [radius=0] - The radius value of the circle.
* @param {Number} [resolution=4] - The number of vertices used to construct the circle.
*/
export class Circle extends Path {
/**
* @name Two.Circle#_flagRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.Circle#radius} needs updating.
*/
private _flagRadius;
/**
* @name Two.Circle#_radius
* @private
* @see {@link Two.Circle#radius}
*/
private _radius;
/**
* @name Two.Circle.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Circle}.
*/
static Properties: ('radius' | string)[];
/**
* @name Two.Circle.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Circle} to create a new instance
* @returns {Two.Circle}
* @description Create a new {@link Two.Circle} from an object notation of a {@link Two.Circle}.
* @nota-bene Works in conjunction with {@link Two.Circle#toObject}
*/
static fromObject(
obj: Parameters[0] & {
radius?: number;
}
): Circle;
constructor(x?: number, y?: number, radius?: number, resolution?: number);
/**
* @name Two.Circle#radius
* @property {Number} - The size of the radius of the circle.
*/
radius: number;
/**
* @name Two.Circle#copy
* @function
* @param {Two.Circle} circle - The reference {@link Two.Circle}
* @description Copy the properties of one {@link Two.Circle} onto another.
*/
copy(circle: Circle): Circle;
/**
* @name Two.Circle#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Circle}
* @description Create a new instance of {@link Two.Circle} with the same properties of the current path.
*/
clone(parent: Group): Circle;
}
import { Path } from 'two.js/src/path';
import { Group } from 'two.js/src/group';
}
================================================
FILE: src/shapes/circle.js
================================================
import { Commands } from '../utils/path-commands.js';
import { HALF_PI, TWO_PI } from '../utils/math.js';
import { Path } from '../path.js';
import { Anchor } from '../anchor.js';
const cos = Math.cos,
sin = Math.sin;
/**
* @name Two.Circle
* @class
* @extends Two.Path
* @param {Number} [x=0] - The x position of the circle.
* @param {Number} [y=0] - The y position of the circle.
* @param {Number} [radius=0] - The radius value of the circle.
* @param {Number} [resolution=4] - The number of vertices used to construct the circle.
*/
export class Circle extends Path {
/**
* @name Two.Circle#_flagRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.Circle#radius} needs updating.
*/
_flagRadius = false;
/**
* @name Two.Circle#_radius
* @private
* @see {@link Two.Circle#radius}
*/
_radius = 0;
constructor(ox, oy, r, resolution) {
// At least 2 vertices are required for proper circlage
const amount = resolution ? Math.max(resolution, 2) : 4;
const points = [];
for (let i = 0; i < amount; i++) {
points.push(new Anchor(0, 0, 0, 0, 0, 0));
}
super(points, true, true, true);
this._renderer.type = 'circle';
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
/**
* @name Two.Circle#radius
* @property {Number} - The size of the radius of the circle.
*/
if (typeof r === 'number') {
this.radius = r;
}
this._update();
if (typeof ox === 'number') {
this.translation.x = ox;
}
if (typeof oy === 'number') {
this.translation.y = oy;
}
}
/**
* @name Two.Circle.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Circle}.
*/
static Properties = ['radius'];
/**
* @name Two.Circle.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Circle} to create a new instance
* @returns {Two.Circle}
* @description Create a new {@link Two.Circle} from an object notation of a {@link Two.Circle}.
* @nota-bene Works in conjunction with {@link Two.Circle#toObject}
*/
static fromObject(obj) {
const circle = new Circle().copy(obj);
if ('id' in obj) {
circle.id = obj.id;
}
return circle;
}
/**
* @name Two.Circle#copy
* @function
* @param {Two.Circle} circle - The reference {@link Two.Circle}
* @description Copy the properties of one {@link Two.Circle} onto another.
*/
copy(circle) {
super.copy.call(this, circle);
for (let i = 0; i < Circle.Properties.length; i++) {
const k = Circle.Properties[i];
if (k in circle && typeof circle[k] === 'number') {
this[k] = circle[k];
}
}
return this;
}
/**
* @name Two.Circle#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagRadius) {
let length = this.vertices.length;
if (!this._closed && length > 2) {
length -= 1;
}
// Coefficient for approximating circular arcs with Bezier curves
const c = (4 / 3) * Math.tan(Math.PI / (length * 2));
const radius = this._radius;
const rc = radius * c;
for (let i = 0; i < this.vertices.length; i++) {
const pct = i / length;
const theta = pct * TWO_PI;
const x = radius * cos(theta);
const y = radius * sin(theta);
const lx = rc * cos(theta - HALF_PI);
const ly = rc * sin(theta - HALF_PI);
const rx = rc * cos(theta + HALF_PI);
const ry = rc * sin(theta + HALF_PI);
const v = this.vertices[i];
v.command = i === 0 ? Commands.move : Commands.curve;
v.set(x, y);
v.controls.left.set(lx, ly);
v.controls.right.set(rx, ry);
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Circle#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagRadius = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Circle#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Circle}
* @description Create a new instance of {@link Two.Circle} with the same properties of the current path.
*/
clone(parent) {
const clone = new Circle(0, 0, this.radius, this.vertices.length);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Circle#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = 'circle';
for (let i = 0; i < Circle.Properties.length; i++) {
const k = Circle.Properties[i];
object[k] = this[k];
}
return object;
}
}
const proto = {
radius: {
enumerable: true,
get: function () {
return this._radius;
},
set: function (v) {
this._radius = v;
this._flagRadius = true;
},
},
};
================================================
FILE: src/shapes/ellipse.d.ts
================================================
declare module 'two.js/src/shapes/ellipse' {
/**
* @name Two.Ellipse
* @class
* @param {Number} [x=0] - The x position of the ellipse.
* @param {Number} [y=0] - The y position of the ellipse.
* @param {Number} [rx=0] - The radius value of the ellipse in the x direction.
* @param {Number} [ry=0] - The radius value of the ellipse in the y direction.
* @param {Number} [resolution=4] - The number of vertices used to construct the ellipse.
*/
export class Ellipse extends Path {
/**
* @name Two.Ellipse.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Ellipse}.
*/
static Properties: ('width' | 'height' | string)[];
/**
* @name Two.Ellipse.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Ellipse} to create a new instance
* @returns {Two.Ellipse}
* @description Create a new {@link Two.Ellipse} from an object notation of a {@link Two.Ellipse}.
* @nota-bene Works in conjunction with {@link Two.Ellipse#toObject}
*/
fromObject(
obj: Parameters[0] & {
width?: number;
height?: number;
}
): Ellipse;
constructor(
x?: number,
y?: number,
rx?: number,
ry?: number,
resolution?: number
);
/**
* @name Two.Ellipse#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Ellipse#width} needs updating.
*/
private _flagWidth;
/**
* @name Two.Ellipse#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Ellipse#height} needs updating.
*/
private _flagHeight;
/**
* @name Two.Ellipse#_width
* @private
* @see {@link Two.Ellipse#width}
*/
private _width;
/**
* @name Two.Ellipse#_height
* @private
* @see {@link Two.Ellipse#height}
*/
private _height;
width: number;
height: number;
/**
* @name Two.Ellipse#copy
* @function
* @param {Two.Ellipse} ellipse - The reference {@link Two.Ellipse}
* @description Copy the properties of one {@link Two.Ellipse} onto another.
*/
copy(ellipse: Ellipse): Ellipse;
}
import { Path } from 'two.js/src/path';
}
================================================
FILE: src/shapes/ellipse.js
================================================
import { Commands } from '../utils/path-commands.js';
import { HALF_PI, TWO_PI } from '../utils/math.js';
import { Path } from '../path.js';
import { Anchor } from '../anchor.js';
const cos = Math.cos,
sin = Math.sin;
/**
* @name Two.Ellipse
* @class
* @extends Two.Path
* @param {Number} [x=0] - The x position of the ellipse.
* @param {Number} [y=0] - The y position of the ellipse.
* @param {Number} [rx=0] - The radius value of the ellipse in the x direction.
* @param {Number} [ry=0] - The radius value of the ellipse in the y direction.
* @param {Number} [resolution=4] - The number of vertices used to construct the ellipse.
*/
export class Ellipse extends Path {
/**
* @name Two.Ellipse#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Ellipse#width} needs updating.
*/
_flagWidth = false;
/**
* @name Two.Ellipse#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Ellipse#height} needs updating.
*/
_flagHeight = false;
/**
* @name Two.Ellipse#_width
* @private
* @see {@link Two.Ellipse#width}
*/
_width = 0;
/**
* @name Two.Ellipse#_height
* @private
* @see {@link Two.Ellipse#height}
*/
_height = 0;
constructor(x, y, rx, ry, resolution) {
if (typeof ry !== 'number' && typeof rx === 'number') {
ry = rx;
}
// At least 2 vertices are required for proper circlage
const amount = resolution ? Math.max(resolution, 2) : 4;
const points = [];
for (let i = 0; i < amount; i++) {
points.push(new Anchor());
}
super(points, true, true, true);
this._renderer.type = 'ellipse';
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
/**
* @name Two.Ellipse#width
* @property {Number} - The width of the ellipse.
*/
if (typeof rx === 'number') {
this.width = rx * 2;
}
/**
* @name Two.Ellipse#height
* @property {Number} - The height of the ellipse.
*/
if (typeof ry === 'number') {
this.height = ry * 2;
}
this._update();
if (typeof x === 'number') {
this.translation.x = x;
}
if (typeof y === 'number') {
this.translation.y = y;
}
}
/**
* @name Two.Ellipse.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Ellipse}.
*/
static Properties = ['width', 'height'];
/**
* @name Two.Ellipse.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Ellipse} to create a new instance
* @returns {Two.Ellipse}
* @description Create a new {@link Two.Ellipse} from an object notation of a {@link Two.Ellipse}.
* @nota-bene Works in conjunction with {@link Two.Ellipse#toObject}
*/
static fromObject(obj) {
const ellipse = new Ellipse().copy(obj);
if ('id' in obj) {
ellipse.id = obj.id;
}
return ellipse;
}
/**
* @name Two.Ellipse#copy
* @function
* @param {Two.Ellipse} ellipse - The reference {@link Two.Ellipse}
* @description Copy the properties of one {@link Two.Ellipse} onto another.
*/
copy(ellipse) {
super.copy.call(this, ellipse);
for (let i = 0; i < Ellipse.Properties.length; i++) {
const k = Ellipse.Properties[i];
if (k in ellipse && typeof ellipse[k] === 'number') {
this[k] = ellipse[k];
}
}
return this;
}
/**
* @name Two.Ellipse#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagWidth || this._flagHeight) {
let length = this.vertices.length;
if (!this._closed && length > 2) {
length -= 1;
}
// Coefficient for approximating circular arcs with Bezier curves
const c = (4 / 3) * Math.tan(Math.PI / (this.vertices.length * 2));
const radiusX = this._width / 2;
const radiusY = this._height / 2;
for (let i = 0; i < this.vertices.length; i++) {
const pct = i / length;
const theta = pct * TWO_PI;
const x = radiusX * cos(theta);
const y = radiusY * sin(theta);
const lx = radiusX * c * cos(theta - HALF_PI);
const ly = radiusY * c * sin(theta - HALF_PI);
const rx = radiusX * c * cos(theta + HALF_PI);
const ry = radiusY * c * sin(theta + HALF_PI);
const v = this.vertices[i];
v.command = i === 0 ? Commands.move : Commands.curve;
v.set(x, y);
v.controls.left.set(lx, ly);
v.controls.right.set(rx, ry);
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Ellipse#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagWidth = this._flagHeight = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Ellipse#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Ellipse}
* @description Create a new instance of {@link Two.Ellipse} with the same properties of the current path.
*/
clone(parent) {
const rx = this.width / 2;
const ry = this.height / 2;
const resolution = this.vertices.length;
const clone = new Ellipse(0, 0, rx, ry, resolution);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Ellipse#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = 'ellipse';
for (let i = 0; i < Ellipse.Properties.length; i++) {
const k = Ellipse.Properties[i];
object[k] = this[k];
}
return object;
}
}
const proto = {
width: {
enumerable: true,
get: function () {
return this._width;
},
set: function (v) {
this._width = v;
this._flagWidth = true;
},
},
height: {
enumerable: true,
get: function () {
return this._height;
},
set: function (v) {
this._height = v;
this._flagHeight = true;
},
},
};
================================================
FILE: src/shapes/line.d.ts
================================================
declare module 'two.js/src/shapes/line' {
/**
* @name Two.Line
* @class
* @param {Number} [x1=0] - The x position of the first vertex on the line.
* @param {Number} [y1=0] - The y position of the first vertex on the line.
* @param {Number} [x2=0] - The x position of the second vertex on the line.
* @param {Number} [y2=0] - The y position of the second vertex on the line.
*/
export class Line extends Path {
static Properties: ('left' | 'right' | string)[];
constructor(x1?: number, y1?: number, x2?: number, y2?: number);
/**
* @name Two.Line#left
* @property {Anchor} - the first vertex on the line.
*/
left: Anchor;
/**
* @name Two.Line#right
* @property {Anchor} - the second vertex on the line.
*/
right: Anchor;
}
import { Path } from 'two.js/src/path';
import { Anchor } from 'two.js/src/anchor';
}
================================================
FILE: src/shapes/line.js
================================================
import { Commands } from '../utils/path-commands.js';
import { TwoError } from '../utils/error.js';
import { _ } from '../utils/underscore.js';
import { Path } from '../path.js';
import { Anchor } from '../anchor.js';
/**
* @name Two.Line
* @class
* @extends Two.Path
* @param {Number} [x1=0] - The x position of the first vertex on the line.
* @param {Number} [y1=0] - The y position of the first vertex on the line.
* @param {Number} [x2=0] - The x position of the second vertex on the line.
* @param {Number} [y2=0] - The y position of the second vertex on the line.
*/
export class Line extends Path {
constructor(x1, y1, x2, y2) {
const points = [new Anchor(x1, y1), new Anchor(x2, y2)];
super(points);
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this.vertices[0].command = Commands.move;
this.vertices[1].command = Commands.line;
this.automatic = false;
}
static Properties = ['left', 'right'];
}
const proto = {
left: {
enumerable: true,
get: function () {
return this.vertices[0];
},
set: function (v) {
if (_.isObject(v)) {
this.vertices.splice(0, 1, v);
this.vertices[0].command = Commands.move;
} else {
const error = new TwoError('Two.Line.left argument is not an object.');
console.warn(error.name, error.message);
}
},
},
right: {
enumerable: true,
get: function () {
return this.vertices[1];
},
set: function (v) {
if (_.isObject(v)) {
this.vertices.splice(1, 1, v);
this.vertices[1].command = Commands.line;
} else {
const error = new TwoError('Two.Line.right argument is not an object.');
console.warn(error.name, error.message);
}
},
},
};
================================================
FILE: src/shapes/points.d.ts
================================================
declare module 'two.js/src/shapes/points' {
/**
* @name Two.Points
* @class
* @param {Vector[]} [vertices] - A list of {@link Two.Vector}s that represent the order and coordinates to construct a rendered set of points.
* @description This is a primary primitive class for quickly and easily drawing points in Two.js. Unless specified methods return their instance of `Two.Points` for the purpose of chaining.
*/
export class Points extends Shape {
static Properties: (
| 'fill'
| 'stroke'
| 'linewidth'
| 'opacity'
| 'visible'
| 'size'
| 'sizeAttenuation'
| 'beginning'
| 'ending'
| 'dashes'
| string
)[];
static fromObject(
obj: Parameters[0] & {
fill?: string;
stroke?: string;
linewidth?: number;
opacity?: number;
visible?: boolean;
size?: number;
sizeAttenuation?: boolean;
beginning?: number;
ending?: number;
dashes: number[] & {
offset?: number;
};
}
): Points;
constructor(vertices?: Vector[]);
private _flagVertices;
private _flagLength;
private _flagFill;
private _flagStroke;
private _flagLinewidth;
private _flagOpacity;
private _flagVisible;
private _flagSize;
private _flagSizeAttenuation;
private _length;
private _fill;
private _stroke;
private _linewidth;
private _opacity;
private _visible;
private _size;
private _sizeAttenuation;
private _beginning;
private _ending;
private _dashes;
private _strokeAttenuation;
/**
* @name Two.Points#size
* @property {Number} - Number describing the diameter each point should have
* @description Set the size of each point in the collection of {@link Two.Points}
*/
size: number;
/**
* @name Two.Points#sizeAttenuation
* @property {Boolean} - Boolean dictating whether Two.js should scale the size of the points based on its matrix hierarcy.
* @description Set to `true` if you'd like the size of the points to be relative to the scale of its parents; `false` to disregard. Default is `false`.
*/
sizeAttenuation: boolean;
/**
* @name Two.Points#beginning
* @property {Number} - Number between zero and one to state the beginning of where the path is rendered.
* @description {@link Two.Points#beginning} is a percentage value that represents at what percentage into the path should the renderer start drawing.
*/
beginning: number;
/**
* @name Two.Points#ending
* @property {Number} - Number between zero and one to state the ending of where the path is rendered.
* @description {@link Two.Points#ending} is a percentage value that represents at what percentage into the path should the renderer start drawing.
*/
ending: number;
/**
* @name Two.Points#fill
* @property {(String|Gradient|Texture)} - The value of what the path should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
fill: string | Gradient | Texture;
/**
* @name Two.Points#stroke
* @property {(String|Gradient|Texture)} - The value of what the path should be outlined in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
stroke: string | Gradient | Texture;
/**
* @name Two.Points#className
* @property {String} - A class to be applied to the element to be compatible with CSS styling.
* @nota-bene Only available for the SVG renderer.
*/
/**
* @name Two.Points#linewidth
* @property {Number} - The thickness in pixels of the stroke.
*/
linewidth: number;
/**
* @name Two.Points#opacity
* @property {Number} - The opaqueness of the path.
* @nota-bene Can be used in conjunction with CSS Colors that have an alpha value.
*/
opacity: number;
className: string;
/**
* @name Two.Points#visible
* @property {Boolean} - Display the points or not.
* @nota-bene For {@link Two.CanvasRenderer} and {@link Two.WebGLRenderer} when set to false all updating is disabled improving performance dramatically with many objects in the scene.
*/
visible: boolean;
/**
* @name Two.Points#vertices
* @property {Vector[]} - An ordered list of vector points for rendering points.
* @description A list of {@link Two.Vector} objects that consist of which coordinates to draw points at.
* @nota-bene The array when manipulating is actually a {@link Two.Collection}.
*/
vertices: (Anchor | Vector)[];
/**
* @name Two.Points#dashes
* @type {number[] & { offset?: number }}
* @property {Number[]} - Array of numbers. Odd indices represent dash length. Even indices represent dash space.
* @description A list of numbers that represent the repeated dash length and dash space applied to the stroke of the text.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray} for more information on the SVG stroke-dasharray attribute.
*/
dashes: number[] & {
offset?: number;
};
/**
* @name Two.Points#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
* @description When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
*/
strokeAttenuation: boolean;
/**
* @name Two.Points#copy
* @function
* @param {Two.Points} points - The reference {@link Two.Points}
* @description Copy the properties of one {@link Two.Points} onto another.
*/
copy(points: Points): Points;
/**
* @name Two.Points#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Points}
* @description Create a new instance of {@link Two.Points} with the same properties of the current path.
*/
clone(parent: Group): Points;
/**
* @name Two.Points#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the points object.
*/
toObject(): object;
/**
* @name Two.Points#dispose
* @function
* @description Release the points' renderer resources and detach all events.
* This method cleans up vertices collection events, individual vertex events,
* and disposes fill/stroke effects (calling dispose() on Gradients and
* Textures for thorough cleanup) while preserving the renderer type for
* potential re-attachment to a new renderer.
*/
dispose(): Points;
/**
* @name Two.Points#noFill
* @function
* @description Short hand method to set fill to `none`.
*/
noFill: () => Points;
/**
* @name Two.Points#noStroke
* @function
* @description Short hand method to set stroke to `none`.
*/
noStroke: () => Points;
/**
* @name Two.Points#corner
* @function
* @description Orient the vertices of the shape to the upper left-hand corner of the points object.
*/
corner: () => Points;
/**
* @name Two.Points#center
* @function
* @description Orient the vertices of the shape to the center of the points object.
*/
center: () => Points;
/**
* @name Two.Points#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the path.
*/
getBoundingClientRect: (shallow?: boolean) => BoundingBox;
/**
* @name Two.Points#subdivide
* @function
* @param {Number} limit - How many times to recurse subdivisions.
* @description Insert a {@link Two.Vector} at the midpoint between every item in {@link Two.Points#vertices}.
*/
subdivide(limit: number): Points;
/**
* @name Two.Points#_updateLength
* @function
* @private
* @param {Number} [limit] -
* @param {Boolean} [silent=false] - If set to `true` then the points object isn't updated before calculation. Useful for internal use.
* @description Recalculate the {@link Two.Points#length} value.
*/
private _updateLength(limit?: number, silent?: boolean): Points;
/**
* @name Two.Points#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
protected _update(bubbles?: boolean): Points;
/**
* @name Two.Points#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset(): Points;
}
import { Shape } from 'two.js/src/shape';
import { Group } from 'two.js/src/group';
import { Gradient } from 'two.js/src/effects/gradient';
import { Texture } from 'two.js/src/effects/texture';
import { BoundingBox } from 'two.js';
import { Vector } from 'two.js/src/vector';
import { Anchor } from 'two.js/src/anchor';
}
================================================
FILE: src/shapes/points.js
================================================
import { subdivide } from '../utils/curves.js';
import { getIdByLength } from '../utils/shape.js';
import { _ } from '../utils/underscore.js';
import { Collection } from '../collection.js';
import { Events } from '../events.js';
import { Anchor } from '../anchor.js';
import { Shape } from '../shape.js';
import {
Path,
FlagVertices,
BindVertices,
UnbindVertices,
FlagFill,
FlagStroke,
} from '../path.js';
import { Gradient } from '../effects/gradient.js';
import { LinearGradient } from '../effects/linear-gradient.js';
import { RadialGradient } from '../effects/radial-gradient.js';
import { Texture } from '../effects/texture.js';
import { getEffectFromObject } from '../utils/shape.js';
const ceil = Math.ceil,
floor = Math.floor;
/**
* @name Two.Points
* @class
* @extends Two.Shape
* @param {Two.Vector[]} [vertices] - A list of {@link Two.Vector}s that represent the order and coordinates to construct a rendered set of points.
* @description This is a primary primitive class for quickly and easily drawing points in Two.js. Unless specified methods return their instance of `Two.Points` for the purpose of chaining.
*/
export class Points extends Shape {
_flagVertices = true;
_flagLength = true;
_flagFill = true;
_flagStroke = true;
_flagLinewidth = true;
_flagOpacity = true;
_flagVisible = true;
_flagSize = true;
_flagSizeAttenuation = true;
_flagStrokeAttenuation = true;
_length = 0;
_fill = '#fff';
_stroke = '#000';
_linewidth = 1;
_opacity = 1.0;
_visible = true;
_size = 1;
_sizeAttenuation = false;
_beginning = 0;
_ending = 1.0;
_dashes = null;
_strokeAttenuation = true;
constructor(vertices) {
super();
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this._renderer.type = 'points';
this._renderer.flagVertices = FlagVertices.bind(this);
this._renderer.bindVertices = BindVertices.bind(this);
this._renderer.unbindVertices = UnbindVertices.bind(this);
this._renderer.flagFill = FlagFill.bind(this);
this._renderer.flagStroke = FlagStroke.bind(this);
this._renderer.vertices = null;
this._renderer.collection = null;
/**
* @name Two.Points#size
* @property {Number} - Number describing the diameter each point should have
* @description Set the size of each point in the collection of {@link Two.Points}
*/
this.size = 1;
/**
* @name Two.Points#sizeAttenuation
* @property {Boolean} - Boolean dictating whether Two.js should scale the size of the points based on its matrix hierarchy.
* @description Set to `true` if you'd like the size of the points to be relative to the scale of its parents; `false` to disregard. Default is `false`.
*/
this.sizeAttenuation = false;
/**
* @name Two.Points#beginning
* @property {Number} - Number between zero and one to state the beginning of where the path is rendered.
* @description {@link Two.Points#beginning} is a percentage value that represents at what percentage into the path should the renderer start drawing.
*/
this.beginning = 0;
/**
* @name Two.Points#ending
* @property {Number} - Number between zero and one to state the ending of where the path is rendered.
* @description {@link Two.Points#ending} is a percentage value that represents at what percentage into the path should the renderer start drawing.
*/
this.ending = 1;
// Style properties
/**
* @name Two.Points#fill
* @property {(String|Two.Gradient|Two.Texture)} - The value of what the path should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
this.fill = '#fff';
/**
* @name Two.Points#stroke
* @property {(String|Two.Gradient|Two.Texture)} - The value of what the path should be outlined in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
this.stroke = '#000';
/**
* @name Two.Points#linewidth
* @property {Number} - The thickness in pixels of the stroke.
*/
this.linewidth = 1;
/**
* @name Two.Points#opacity
* @property {Number} - The opaqueness of the path.
* @nota-bene Can be used in conjunction with CSS Colors that have an alpha value.
*/
this.opacity = 1;
/**
* @name Two.Points#className
* @property {String} - A class to be applied to the element to be compatible with CSS styling.
* @nota-bene Only rendered to DOM in the SVG renderer.
*/
this.className = '';
/**
* @name Two.Points#visible
* @property {Boolean} - Display the points or not.
* @nota-bene For {@link Two.CanvasRenderer} and {@link Two.WebGLRenderer} when set to false all updating is disabled improving performance dramatically with many objects in the scene.
*/
this.visible = true;
/**
* @name Two.Points#vertices
* @property {Two.Vector[]} - An ordered list of vector points for rendering points.
* @description A list of {@link Two.Vector} objects that consist of which coordinates to draw points at.
* @nota-bene The array when manipulating is actually a {@link Two.Collection}.
*/
this.vertices = vertices;
/**
* @name Two.Points#dashes
* @property {Number[]} - Array of numbers. Odd indices represent dash length. Even indices represent dash space.
* @description A list of numbers that represent the repeated dash length and dash space applied to the stroke of the text.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray} for more information on the SVG stroke-dasharray attribute.
*/
this.dashes = [];
/**
* @name Two.Points#dashes#offset
* @property {Number} - A number in pixels to offset {@link Two.Points#dashes} display.
*/
this.dashes.offset = 0;
}
/**
* @name Two.Points.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Points}.
*/
static Properties = [
'fill',
'stroke',
'linewidth',
'opacity',
'visible',
'size',
'sizeAttenuation',
'beginning',
'ending',
'dashes',
'strokeAttenuation',
];
/**
* @name Two.Points.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Points} to create a new instance
* @returns {Two.Points}
* @description Create a new {@link Two.Points} from an object notation of a {@link Two.Points}.
* @nota-bene Works in conjunction with {@link Two.Points#toObject}
*/
static fromObject(obj) {
const fill =
typeof obj.fill === 'string' ? obj.fill : getEffectFromObject(obj.fill);
const stroke =
typeof obj.stroke === 'string'
? obj.stroke
: getEffectFromObject(obj.stroke);
const points = new Points().copy({ ...obj, fill, stroke });
if ('id' in obj) {
points.id = obj.id;
}
return points;
}
/**
* @name Two.Points#copy
* @function
* @param {Two.Points} points - The reference {@link Two.Points}
* @description Copy the properties of one {@link Two.Points} onto another.
*/
copy(points) {
super.copy.call(this, points);
for (let j = 0; j < points.vertices.length; j++) {
const v = points.vertices[j];
if (v instanceof Anchor) {
this.vertices.push(points.vertices[j].clone());
} else {
this.vertices.push(new Anchor().copy(v));
}
}
for (let i = 0; i < Points.Properties.length; i++) {
const k = Points.Properties[i];
if (k in points) {
this[k] = points[k];
}
}
return this;
}
/**
* @name Two.Points#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Points}
* @description Create a new instance of {@link Two.Points} with the same properties of the current path.
*/
clone(parent) {
const clone = new Points();
for (let j = 0; j < this.vertices.length; j++) {
clone.vertices.push(this.vertices[j].clone());
}
for (let i = 0; i < Points.Properties.length; i++) {
const k = Points.Properties[i];
clone[k] = this[k];
}
clone.className = this.className;
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
if (parent) {
parent.add(clone);
}
return clone._update();
}
/**
* @name Two.Points#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the points object.
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = 'points';
result.vertices = this.vertices.map((v) => v.toObject());
_.each(
Points.Properties,
function (k) {
if (typeof this[k] !== 'undefined') {
if (this[k].toObject) {
result[k] = this[k].toObject();
} else {
result[k] = this[k];
}
}
},
this
);
return result;
}
/**
* @name Two.Points#dispose
* @function
* @returns {Two.Points}
* @description Release the points' renderer resources and detach all events.
* This method cleans up vertices collection events, individual vertex events,
* and disposes fill/stroke effects (calling dispose() on Gradients and
* Textures for thorough cleanup) while preserving the renderer type for
* potential re-attachment to a new renderer.
*/
dispose() {
// Call parent dispose to preserve renderer type and unbind events
super.dispose();
// Unbind vertices collection events
if (this.vertices && typeof this.vertices.unbind === 'function') {
try {
this.vertices.unbind();
} catch (e) {
// Ignore unbind errors for incomplete Collection objects
}
}
// Unbind individual vertex events
if (this.vertices) {
for (let i = 0; i < this.vertices.length; i++) {
const v = this.vertices[i];
if (typeof v.unbind === 'function') {
v.unbind();
}
}
}
// Dispose fill effect (more thorough than unbind)
if (
typeof this.fill === 'object' &&
typeof this.fill.dispose === 'function'
) {
this.fill.dispose();
} else if (
typeof this.fill === 'object' &&
typeof this.fill.unbind === 'function'
) {
this.fill.unbind();
}
// Dispose stroke effect (more thorough than unbind)
if (
typeof this.stroke === 'object' &&
typeof this.stroke.dispose === 'function'
) {
this.stroke.dispose();
} else if (
typeof this.stroke === 'object' &&
typeof this.stroke.unbind === 'function'
) {
this.stroke.unbind();
}
return this;
}
/**
* @name Two.Points#noFill
* @function
* @description Short hand method to set fill to `none`.
*/
noFill = Path.prototype.noFill;
/**
* @name Two.Points#noStroke
* @function
* @description Short hand method to set stroke to `none`.
*/
noStroke = Path.prototype.noStroke;
/**
* @name Two.Points#corner
* @function
* @description Orient the vertices of the shape to the upper left-hand corner of the points object.
*/
corner = Path.prototype.corner;
/**
* @name Two.Points#center
* @function
* @description Orient the vertices of the shape to the center of the points object.
*/
center = Path.prototype.center;
/**
* @name Two.Points#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the path.
*/
getBoundingClientRect = Path.prototype.getBoundingClientRect;
/**
* @name Two.Points#subdivide
* @function
* @param {Number} limit - How many times to recurse subdivisions.
* @description Insert a {@link Two.Vector} at the midpoint between every item in {@link Two.Points#vertices}.
*/
subdivide(limit) {
// TODO: DRYness (function below)
this._update();
let points = [];
for (let i = 0; i < this.vertices.length; i++) {
const a = this.vertices[i];
const b = this.vertices[i - 1];
if (!b) {
continue;
}
const x1 = a.x;
const y1 = a.y;
const x2 = b.x;
const y2 = b.y;
const subdivisions = subdivide(x1, y1, x1, y1, x2, y2, x2, y2, limit);
points = points.concat(subdivisions);
}
this.vertices = points;
return this;
}
/**
* @name Two.Points#_updateLength
* @function
* @private
* @param {Number} [limit] -
* @param {Boolean} [silent=false] - If set to `true` then the points object isn't updated before calculation. Useful for internal use.
* @description Recalculate the {@link Two.Points#length} value.
*/
_updateLength = Path.prototype._updateLength;
/**
* @name Two.Points#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices) {
if (this._flagLength) {
this._updateLength(undefined, true);
}
const beginning = Math.min(this._beginning, this._ending);
const ending = Math.max(this._beginning, this._ending);
const bid = getIdByLength(this, beginning * this._length);
const eid = getIdByLength(this, ending * this._length);
const low = ceil(bid);
const high = floor(eid);
let j = 0,
v;
this._renderer.vertices = [];
this._renderer.collection = [];
for (let i = 0; i < this._collection.length; i++) {
if (i >= low && i <= high) {
v = this._collection[i];
this._renderer.collection.push(v);
this._renderer.vertices[j * 2 + 0] = v.x;
this._renderer.vertices[j * 2 + 1] = v.y;
j++;
}
}
}
super._update.apply(this, arguments);
return this;
}
/**
* @name Two.Points#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagVertices =
this._flagLength =
this._flagFill =
this._flagStroke =
this._flagLinewidth =
this._flagOpacity =
this._flagVisible =
this._flagSize =
this._flagSizeAttenuation =
false;
super.flagReset.call(this);
return this;
}
}
const proto = {
linewidth: {
enumerable: true,
get: function () {
return this._linewidth;
},
set: function (v) {
this._linewidth = v;
this._flagLinewidth = true;
},
},
opacity: {
enumerable: true,
get: function () {
return this._opacity;
},
set: function (v) {
this._opacity = v;
this._flagOpacity = true;
},
},
visible: {
enumerable: true,
get: function () {
return this._visible;
},
set: function (v) {
this._visible = v;
this._flagVisible = true;
},
},
size: {
enumerable: true,
get: function () {
return this._size;
},
set: function (v) {
this._size = v;
this._flagSize = true;
},
},
sizeAttenuation: {
enumerable: true,
get: function () {
return this._sizeAttenuation;
},
set: function (v) {
this._sizeAttenuation = v;
this._flagSizeAttenuation = true;
},
},
fill: {
enumerable: true,
get: function () {
return this._fill;
},
set: function (f) {
if (
this._fill instanceof Gradient ||
this._fill instanceof LinearGradient ||
this._fill instanceof RadialGradient ||
this._fill instanceof Texture
) {
this._fill.unbind(Events.Types.change, this._renderer.flagFill);
}
this._fill = f;
this._flagFill = true;
if (
this._fill instanceof Gradient ||
this._fill instanceof LinearGradient ||
this._fill instanceof RadialGradient ||
this._fill instanceof Texture
) {
this._fill.bind(Events.Types.change, this._renderer.flagFill);
}
},
},
stroke: {
enumerable: true,
get: function () {
return this._stroke;
},
set: function (f) {
if (
this._stroke instanceof Gradient ||
this._stroke instanceof LinearGradient ||
this._stroke instanceof RadialGradient ||
this._stroke instanceof Texture
) {
this._stroke.unbind(Events.Types.change, this._renderer.flagStroke);
}
this._stroke = f;
this._flagStroke = true;
if (
this._stroke instanceof Gradient ||
this._stroke instanceof LinearGradient ||
this._stroke instanceof RadialGradient ||
this._stroke instanceof Texture
) {
this._stroke.bind(Events.Types.change, this._renderer.flagStroke);
}
},
},
/**
* @name Two.Points#length
* @property {Number} - The sum of distances between all {@link Two.Points#vertices}.
*/
length: {
get: function () {
if (this._flagLength) {
this._updateLength();
}
return this._length;
},
},
beginning: {
enumerable: true,
get: function () {
return this._beginning;
},
set: function (v) {
this._beginning = v;
this._flagVertices = true;
},
},
ending: {
enumerable: true,
get: function () {
return this._ending;
},
set: function (v) {
this._ending = v;
this._flagVertices = true;
},
},
vertices: {
enumerable: true,
get: function () {
return this._collection;
},
set: function (vertices) {
const bindVertices = this._renderer.bindVertices;
const unbindVertices = this._renderer.unbindVertices;
// Remove previous listeners
if (this._collection) {
this._collection
.unbind(Events.Types.insert, bindVertices)
.unbind(Events.Types.remove, unbindVertices);
}
// Create new Collection with copy of vertices
if (vertices instanceof Collection) {
this._collection = vertices;
} else {
this._collection = new Collection(vertices || []);
}
// Listen for Collection changes and bind / unbind
this._collection
.bind(Events.Types.insert, bindVertices)
.bind(Events.Types.remove, unbindVertices);
// Bind Initial Vertices
bindVertices(this._collection);
},
},
dashes: {
enumerable: true,
get: function () {
return this._dashes;
},
set: function (v) {
if (typeof v.offset !== 'number') {
v.offset = (this.dashes && this._dashes.offset) || 0;
}
this._dashes = v;
},
},
/**
* @name Two.Points#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
* @description When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
*/
strokeAttenuation: {
enumerable: true,
get: function () {
return this._strokeAttenuation;
},
set: function (v) {
this._strokeAttenuation = !!v;
this._flagStrokeAttenuation = true;
this._flagLinewidth = true;
},
},
};
================================================
FILE: src/shapes/polygon.d.ts
================================================
declare module 'two.js/src/shapes/polygon' {
/**
* @name Two.Polygon
* @class
* @param {Number} [x=0] - The x position of the polygon.
* @param {Number} [y=0] - The y position of the polygon.
* @param {Number} [radius=0] - The radius value of the polygon.
* @param {Number} [sides=12] - The number of vertices used to construct the polygon.
*/
export class Polygon extends Path {
/**
* @name Two.Polygon.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Polygon}.
*/
static Properties: ('width' | 'height' | 'sides' | string)[];
/**
* @name Two.Polygon.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Polygon} to create a new instance
* @returns {Two.Polygon}
* @description Create a new {@link Two.Polygon} from an object notation of a {@link Two.Polygon}.
* @nota-bene Works in conjunction with {@link Two.Polygon#toObject}
*/
static fromObject(
obj: Parameters[0] & {
width?: number;
height?: number;
sides?: number;
}
): Polygon;
/**
* @name Two.Polygon#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Polygon#width} needs updating.
*/
private _flagWidth;
/**
* @name Two.Polygon#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Polygon#height} needs updating.
*/
private _flagHeight;
/**
* @name Two.Polygon#_flagSides
* @private
* @property {Boolean} - Determines whether the {@link Two.Polygon#sides} needs updating.
*/
private _flagSides;
/**
* @name Two.Polygon#_width
* @private
* @see {@link Two.Polygon#width}
*/
private _width;
/**
* @name Two.Polygon#_height
* @private
* @see {@link Two.Polygon#height}
*/
private _height;
/**
* @name Two.Polygon#_sides
* @private
* @see {@link Two.Polygon#sides}
*/
private _sides;
constructor(x?: number, y?: number, radius?: number, sides?: number);
closed: boolean;
width: number;
height: number;
sides: number;
/**
* @name Two.Polygon#copy
* @function
* @param {Two.Polygon} polygon - The reference {@link Two.Polygon}
* @description Copy the properties of one {@link Two.Polygon} onto another.
*/
copy(polygon: Polygon): Polygon;
/**
* @name Two.Polygon#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Polygon}
* @description Create a new instance of {@link Two.Polygon} with the same properties of the current path.
*/
clone(parent: Group): Polygon;
}
import { Path } from 'two.js/src/path';
import { Group } from 'two.js/src/group';
}
================================================
FILE: src/shapes/polygon.js
================================================
import { Commands } from '../utils/path-commands.js';
import { TWO_PI } from '../utils/math.js';
import { Path } from '../path.js';
import { Anchor } from '../anchor.js';
const cos = Math.cos,
sin = Math.sin;
/**
* @name Two.Polygon
* @class
* @extends Two.Path
* @param {Number} [x=0] - The x position of the polygon.
* @param {Number} [y=0] - The y position of the polygon.
* @param {Number} [radius=0] - The radius value of the polygon.
* @param {Number} [sides=12] - The number of vertices used to construct the polygon.
*/
export class Polygon extends Path {
/**
* @name Two.Polygon#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Polygon#width} needs updating.
*/
_flagWidth = false;
/**
* @name Two.Polygon#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Polygon#height} needs updating.
*/
_flagHeight = false;
/**
* @name Two.Polygon#_flagSides
* @private
* @property {Boolean} - Determines whether the {@link Two.Polygon#sides} needs updating.
*/
_flagSides = false;
/**
* @name Two.Polygon#_radius
* @private
* @see {@link Two.Polygon#radius}
*/
_radius = 0;
/**
* @name Two.Polygon#_width
* @private
* @see {@link Two.Polygon#width}
*/
_width = 0;
/**
* @name Two.Polygon#_height
* @private
* @see {@link Two.Polygon#height}
*/
_height = 0;
/**
* @name Two.Polygon#_sides
* @private
* @see {@link Two.Polygon#sides}
*/
_sides = 0;
constructor(x, y, radius, sides) {
sides = Math.max(sides || 0, 3);
super();
this._renderer.type = 'polygon';
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this.closed = true;
this.automatic = false;
/**
* @name Two.Polygon#radius
* @property {Number} - The radius value of the polygon.
* @nota-bene This property is tied to {@link Two.Polygon#width} and {@link Two.Polygon#height}. When you set `radius`, it affects `width` and `height`. Likewise, if you set `width` or `height` it will change the `radius`.
*/
if (typeof radius === 'number') {
this.radius = radius;
}
/**
* @name Two.Polygon#width
* @property {Number} - The size of the width of the polygon.
* @nota-bene This property is tied to {@link Two.Polygon#radius}. When you set `radius`, it affects the `width`. Likewise, if you set `width` it will change the `radius`.
*/
/**
* @name Two.Polygon#height
* @property {Number} - The size of the height of the polygon.
* @nota-bene This property is tied to {@link Two.Polygon#radius}. When you set `radius`, it affects the `height`. Likewise, if you set `height` it will change the `radius`.
*/
/**
* @name Two.Polygon#sides
* @property {Number} - The amount of sides the polyogn has.
*/
if (typeof sides === 'number') {
this.sides = sides;
}
this._update();
if (typeof x === 'number') {
this.translation.x = x;
}
if (typeof y === 'number') {
this.translation.y = y;
}
}
/**
* @name Two.Polygon.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Polygon}.
*/
static Properties = ['width', 'height', 'sides'];
/**
* @name Two.Polygon.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Polygon} to create a new instance
* @returns {Two.Polygon}
* @description Create a new {@link Two.Polygon} from an object notation of a {@link Two.Polygon}.
* @nota-bene Works in conjunction with {@link Two.Polygon#toObject}
*/
static fromObject(obj) {
const polygon = new Polygon().copy(obj);
if ('id' in obj) {
polygon.id = obj.id;
}
return polygon;
}
/**
* @name Two.Polygon#copy
* @function
* @param {Two.Polygon} polygon - The reference {@link Two.Polygon}
* @description Copy the properties of one {@link Two.Polygon} onto another.
*/
copy(polygon) {
super.copy.call(this, polygon);
for (let i = 0; i < Polygon.Properties.length; i++) {
const k = Polygon.Properties[i];
if (k in polygon && typeof polygon[k] === 'number') {
this[k] = polygon[k];
}
}
return this;
}
/**
* @name Two.Polygon#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (
this._flagVertices ||
this._flagWidth ||
this._flagHeight ||
this._flagSides
) {
const sides = this._sides;
const amount = sides + 1;
let length = this.vertices.length;
if (length > sides) {
this.vertices.splice(sides - 1, length - sides);
length = sides;
}
for (let i = 0; i < amount; i++) {
const pct = (i + 0.5) / sides;
const theta = TWO_PI * pct + Math.PI / 2;
const x = (this._width * cos(theta)) / 2;
const y = (this._height * sin(theta)) / 2;
if (i >= length) {
this.vertices.push(new Anchor(x, y));
} else {
this.vertices[i].set(x, y);
}
this.vertices[i].command = i === 0 ? Commands.move : Commands.line;
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Polygon#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagWidth = this._flagHeight = this._flagSides = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Polygon#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Polygon}
* @description Create a new instance of {@link Two.Polygon} with the same properties of the current path.
*/
clone(parent) {
const clone = new Polygon(0, 0, 0, this.sides);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
clone.width = this.width;
clone.height = this.height;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Polygon#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = 'polygon';
for (let i = 0; i < Polygon.Properties.length; i++) {
const k = Polygon.Properties[i];
object[k] = this[k];
}
return object;
}
}
const proto = {
radius: {
enumerable: true,
get: function () {
return this._radius;
},
set: function (v) {
this._radius = v;
this.width = v * 2;
this.height = v * 2;
},
},
width: {
enumerable: true,
get: function () {
return this._width;
},
set: function (v) {
this._width = v;
this._flagWidth = true;
this._radius = Math.max(this.width, this.height) / 2;
},
},
height: {
enumerable: true,
get: function () {
return this._height;
},
set: function (v) {
this._height = v;
this._flagHeight = true;
this._radius = Math.max(this.width, this.height) / 2;
},
},
sides: {
enumerable: true,
get: function () {
return this._sides;
},
set: function (v) {
this._sides = v;
this._flagSides = true;
},
},
};
================================================
FILE: src/shapes/rectangle.d.ts
================================================
declare module 'two.js/src/shapes/rectangle' {
/**
* @name Two.Rectangle
* @class
* @param {Number} [x=0] - The x position of the rectangle.
* @param {Number} [y=0] - The y position of the rectangle.
* @param {Number} [width=1] - The width value of the rectangle.
* @param {Number} [height=1] - The width value of the rectangle.
*/
export class Rectangle extends Path {
/**
* @name Two.Rectangle.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Rectangle}.
*/
static Properties: ('width' | 'height' | 'origin' | string)[];
/**
* @name Two.Rectangle.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Rectangle} to create a new instance
* @returns {Two.Rectangle}
* @description Create a new {@link Two.Rectangle} from an object notation of a {@link Two.Rectangle}.
* @nota-bene Works in conjunction with {@link Two.Rectangle#toObject}
*/
static fromObject(
obj: Parameters[0] & {
width?: number;
height?: number;
origin?: { x: number; y: number } | Vector;
}
): Rectangle;
constructor(x?: number, y?: number, width?: number, height?: number);
/**
* @name Two.Rectangle#width
* @property {Number} - The size of the width of the rectangle.
*/
width: number;
/**
* @name Two.Rectangle#height
* @property {Number} - The size of the height of the rectangle.
*/
height: number;
/**
* @name Two.Rectangle#origin
* @property {Number} - A two-component vector describing the origin offset to draw the rectangle. Default is `0, 0`.
*/
origin: Vector;
/**
* @name Two.Rectangle#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Rectangle#width} needs updating.
*/
private _flagWidth;
/**
* @name Two.Rectangle#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Rectangle#height} needs updating.
*/
private _flagHeight;
/**
* @name Two.Rectangle#_width
* @private
* @see {@link Two.Rectangle#width}
*/
private _width;
/**
* @name Two.Rectangle#_height
* @private
* @see {@link Two.Rectangle#height}
*/
private _height;
private _origin: Vector;
/**
* @name Two.Rectangle#copy
* @function
* @param {Two.Rectangle} rectangle - The reference {@link Two.Rectangle}
* @description Copy the properties of one {@link Two.Rectangle} onto another.
*/
copy(rectangle: Rectangle): Rectangle;
/**
* @name Two.Rectangle#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Rectangle}
* @description Create a new instance of {@link Two.Rectangle} with the same properties of the current path.
*/
clone(parent: Group): Rectangle;
}
import { Path } from 'two.js/src/path';
import { Vector } from 'two.js/src/vector';
import { Group } from 'two.js/src/group';
}
================================================
FILE: src/shapes/rectangle.js
================================================
import { Commands } from '../utils/path-commands.js';
import { Events } from '../events.js';
import { Path } from '../path.js';
import { Anchor } from '../anchor.js';
import { Vector } from '../vector.js';
/**
* @name Two.Rectangle
* @class
* @extends Two.Path
* @param {Number} [x=0] - The x position of the rectangle.
* @param {Number} [y=0] - The y position of the rectangle.
* @param {Number} [width=1] - The width value of the rectangle.
* @param {Number} [height=1] - The width value of the rectangle.
*/
export class Rectangle extends Path {
constructor(x, y, width, height) {
const points = [
new Anchor(),
new Anchor(),
new Anchor(),
new Anchor(),
// new Anchor() // TODO: Figure out how to handle this for `beginning` / `ending` animations
];
super(points, true, false, true);
this._renderer.type = 'rectangle';
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
/**
* @name Two.Rectangle#width
* @property {Number} - The size of the width of the rectangle.
*/
this.width = typeof width === 'number' ? width : 1;
/**
* @name Two.Rectangle#height
* @property {Number} - The size of the height of the rectangle.
*/
this.height = typeof height === 'number' ? height : 1;
/**
* @name Two.Rectangle#origin
* @property {Number} - A two-component vector describing the origin offset to draw the rectangle. Default is `0, 0`.
*/
this.origin = new Vector();
if (typeof x === 'number') {
this.translation.x = x;
}
if (typeof y === 'number') {
this.translation.y = y;
}
this._update();
}
/**
* @name Two.Rectangle.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Rectangle}.
*/
static Properties = ['width', 'height', 'origin'];
/**
* @name Two.Rectangle.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Rectangle} to create a new instance
* @returns {Two.Rectangle}
* @description Create a new {@link Two.Rectangle} from an object notation of a {@link Two.Rectangle}.
* @nota-bene Works in conjunction with {@link Two.Rectangle#toObject}
*/
static fromObject(obj) {
const rectangle = new Rectangle().copy(obj);
if ('id' in obj) {
rectangle.id = obj.id;
}
return rectangle;
}
/**
* @name Two.Rectangle#copy
* @function
* @param {Two.Rectangle} rectangle - The reference {@link Two.Rectangle}
* @description Copy the properties of one {@link Two.Rectangle} onto another.
*/
copy(rectangle) {
super.copy.call(this, rectangle);
for (let i = 0; i < Rectangle.Properties.length; i++) {
const k = Rectangle.Properties[i];
if (k in rectangle) {
if (typeof rectangle[k] === 'number') {
this[k] = rectangle[k];
} else if (this[k] instanceof Vector) {
this[k].copy(rectangle[k]);
}
}
}
return this;
}
/**
* @name Two.Rectangle#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Rectangle#width} needs updating.
*/
_flagWidth = false;
/**
* @name Two.Rectangle#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Rectangle#height} needs updating.
*/
_flagHeight = false;
/**
* @name Two.Rectangle#_width
* @private
* @see {@link Two.Rectangle#width}
*/
_width = 0;
/**
* @name Two.Rectangle#_height
* @private
* @see {@link Two.Rectangle#height}
*/
_height = 0;
_origin = null;
/**
* @name Two.Rectangle#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (this._flagVertices || this._flagWidth || this._flagHeight) {
const xr = this._width / 2;
const yr = this._height / 2;
if (!this._closed && this.vertices.length === 4) {
this.vertices.push(new Anchor());
}
this.vertices[0].set(-xr, -yr).sub(this._origin).command = Commands.move;
this.vertices[1].set(xr, -yr).sub(this._origin).command = Commands.line;
this.vertices[2].set(xr, yr).sub(this._origin).command = Commands.line;
this.vertices[3].set(-xr, yr).sub(this._origin).command = Commands.line;
// FYI: Two.Sprite and Two.ImageSequence have 4 verts
if (this.vertices[4]) {
this.vertices[4].set(-xr, -yr).sub(this._origin).command =
Commands.line;
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Rectangle#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagWidth = this._flagHeight = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Rectangle#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Rectangle}
* @description Create a new instance of {@link Two.Rectangle} with the same properties of the current path.
*/
clone(parent) {
const clone = new Rectangle(0, 0, this.width, this.height);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
if (clone[k] instanceof Vector) {
clone[k].copy(this[k]);
} else {
clone[k] = this[k];
}
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Rectangle#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = 'rectangle';
object.width = this.width;
object.height = this.height;
object.origin = this.origin.toObject();
return object;
}
}
const proto = {
width: {
enumerable: true,
get: function () {
return this._width;
},
set: function (v) {
this._width = v;
this._flagWidth = true;
},
},
height: {
enumerable: true,
get: function () {
return this._height;
},
set: function (v) {
this._height = v;
this._flagHeight = true;
},
},
origin: {
enumerable: true,
get: function () {
return this._origin;
},
set: function (v) {
if (this._origin) {
this._origin.unbind(Events.Types.change, this._renderer.flagVertices);
}
this._origin = v;
this._origin.bind(Events.Types.change, this._renderer.flagVertices);
this._renderer.flagVertices();
},
},
};
================================================
FILE: src/shapes/rounded-rectangle.d.ts
================================================
declare module 'two.js/src/shapes/rounded-rectangle' {
/**
* @name Two.RoundedRectangle
* @class
* @param {Number} [x=0] - The x position of the rounded rectangle.
* @param {Number} [y=0] - The y position of the rounded rectangle.
* @param {Number} [width=0] - The width value of the rounded rectangle.
* @param {Number} [height=0] - The width value of the rounded rectangle.
* @param {Number} [radius=0] - The radius value of the rounded rectangle.
* @param {Number} [resolution=12] - The number of vertices used to construct the rounded rectangle.
*/
export class RoundedRectangle extends Path {
/**
* @name Two.RoundedRectangle.Properties
* @property {String[]} - A list of properties that are on every {@link Two.RoundedRectangle}.
*/
static Properties: ('width' | 'height' | 'radius' | string)[];
/**
* @name Two.RoundedRectangle.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.RoundedRectangle} to create a new instance
* @returns {Two.RoundedRectangle}
* @description Create a new {@link Two.RoundedRectangle} from an object notation of a {@link Two.RoundedRectangle}.
* @nota-bene Works in conjunction with {@link Two.RoundedRectangle#toObject}
*/
fromObject(
obj: Parameters[0] & {
width?: number;
height?: number;
radius?: number;
}
): RoundedRectangle;
constructor(
x?: number,
y?: number,
width?: number,
height?: number,
radius?: number | Vector
);
/**
* @name Two.RoundedRectangle#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.RoundedRectangle#width} needs updating.
*/
private _flagWidth;
/**
* @name Two.RoundedRectangle#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.RoundedRectangle#height} needs updating.
*/
private _flagHeight;
/**
* @name Two.RoundedRectangle#_flagRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.RoundedRectangle#radius} needs updating.
*/
private _flagRadius;
/**
* @name Two.RoundedRectangle#_width
* @private
* @see {@link Two.RoundedRectangle#width}
*/
private _width;
/**
* @name Two.RoundedRectangle#_height
* @private
* @see {@link Two.RoundedRectangle#height}
*/
private _height;
/**
* @name Two.RoundedRectangle#_radius
* @private
* @see {@link Two.RoundedRectangle#radius}
*/
private _radius;
width: number;
height: number;
radius: number | Vector;
/**
* @name Two.RoundedRectangle.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.RoundedRectangle} to create a new instance
* @returns {Two.RoundedRectangle}
* @description Create a new {@link Two.RoundedRectangle} from an object notation of a {@link Two.RoundedRectangle}.
* @nota-bene Works in conjunction with {@link Two.RoundedRectangle#toObject}
*/
copy(roundedRectangle: RoundedRectangle): RoundedRectangle;
}
import { Path } from 'two.js/src/path';
import { Vector } from 'two.js/src/vector';
}
================================================
FILE: src/shapes/rounded-rectangle.js
================================================
import { Commands } from '../utils/path-commands.js';
import { Events } from '../events.js';
import { Path } from '../path.js';
import { Anchor } from '../anchor.js';
import { Vector } from '../vector.js';
/**
* @name Two.RoundedRectangle
* @class
* @extends Two.Path
* @param {Number} [x=0] - The x position of the rounded rectangle.
* @param {Number} [y=0] - The y position of the rounded rectangle.
* @param {Number} [width=0] - The width value of the rounded rectangle.
* @param {Number} [height=0] - The width value of the rounded rectangle.
* @param {Number|Two.Vector} [radius=0] - The radius value of the rounded rectangle.
* @param {Number} [resolution=12] - The number of vertices used to construct the rounded rectangle.
*/
export class RoundedRectangle extends Path {
/**
* @name Two.RoundedRectangle#_flagWidth
* @private
* @property {Boolean} - Determines whether the {@link Two.RoundedRectangle#width} needs updating.
*/
_flagWidth = false;
/**
* @name Two.RoundedRectangle#_flagHeight
* @private
* @property {Boolean} - Determines whether the {@link Two.RoundedRectangle#height} needs updating.
*/
_flagHeight = false;
/**
* @name Two.RoundedRectangle#_flagRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.RoundedRectangle#radius} needs updating.
*/
_flagRadius = false;
/**
* @name Two.RoundedRectangle#_width
* @private
* @see {@link Two.RoundedRectangle#width}
*/
_width = 0;
/**
* @name Two.RoundedRectangle#_height
* @private
* @see {@link Two.RoundedRectangle#height}
*/
_height = 0;
/**
* @name Two.RoundedRectangle#_radius
* @private
* @see {@link Two.RoundedRectangle#radius}
*/
_radius = 12;
constructor(x, y, width, height, radius) {
if (
typeof radius === 'undefined' &&
typeof width === 'number' &&
typeof height === 'number'
) {
radius = Math.floor(Math.min(width, height) / 12);
}
const points = [];
for (let i = 0; i < 10; i++) {
points.push(
new Anchor(0, 0, 0, 0, 0, 0, i === 0 ? Commands.move : Commands.curve)
);
}
// points[points.length - 1].command = Two.Commands.close;
super(points);
this._renderer.type = 'rounded-rectangle';
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this.closed = true;
this.automatic = false;
this._renderer.flagRadius = FlagRadius.bind(this);
/**
* @name Two.RoundedRectangle#width
* @property {Number} - The width of the rounded rectangle.
*/
if (typeof width === 'number') {
this.width = width;
}
/**
* @name Two.RoundedRectangle#height
* @property {Number} - The height of the rounded rectangle.
*/
if (typeof height === 'number') {
this.height = height;
}
/**
* @name Two.RoundedRectangle#radius
* @property {Number} - The size of the radius of the rounded rectangle.
*/
if (typeof radius === 'number' || radius instanceof Vector) {
this.radius = radius;
}
this._update();
if (typeof x === 'number') {
this.translation.x = x;
}
if (typeof y === 'number') {
this.translation.y = y;
}
}
/**
* @name Two.RoundedRectangle.Properties
* @property {String[]} - A list of properties that are on every {@link Two.RoundedRectangle}.
*/
static Properties = ['width', 'height', 'radius'];
/**
* @name Two.RoundedRectangle.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.RoundedRectangle} to create a new instance
* @returns {Two.RoundedRectangle}
* @description Create a new {@link Two.RoundedRectangle} from an object notation of a {@link Two.RoundedRectangle}.
* @nota-bene Works in conjunction with {@link Two.RoundedRectangle#toObject}
*/
static fromObject(obj) {
const rectangle = new RoundedRectangle().copy(obj);
if ('id' in obj) {
rectangle.id = obj.id;
}
return rectangle;
}
/**
* @name Two.RoundedRectangle#copy
* @function
* @param {Two.RoundedRectangle} roundedRectangle - The reference {@link Two.RoundedRectangle}
* @description Copy the properties of one {@link Two.RoundedRectangle} onto another.
*/
copy(roundedRectangle) {
super.copy.call(this, roundedRectangle);
for (let i = 0; i < RoundedRectangle.Properties.length; i++) {
const k = RoundedRectangle.Properties[i];
if (k in roundedRectangle) {
const value = roundedRectangle[k];
if (/radius/i.test(k)) {
this[k] =
typeof value === 'number' || value instanceof Vector
? value
: new Vector().copy(value);
} else if (typeof value === 'number') {
this[k] = value;
}
}
}
return this;
}
/**
* @name Two.RoundedRectangle#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (
this._flagVertices ||
this._flagWidth ||
this._flagHeight ||
this._flagRadius
) {
const width = this._width;
const height = this._height;
let rx, ry;
if (this._radius instanceof Vector) {
rx = this._radius.x;
ry = this._radius.y;
} else {
rx = this._radius;
ry = this._radius;
}
let v;
let w = width / 2;
let h = height / 2;
v = this.vertices[0];
v.x = -(w - rx);
v.y = -h;
// Upper Right Corner
v = this.vertices[1];
v.x = w - rx;
v.y = -h;
v.controls.left.clear();
v.controls.right.x = rx;
v.controls.right.y = 0;
v = this.vertices[2];
v.x = w;
v.y = -(h - ry);
v.controls.right.clear();
v.controls.left.clear();
// Bottom Right Corner
v = this.vertices[3];
v.x = w;
v.y = h - ry;
v.controls.left.clear();
v.controls.right.x = 0;
v.controls.right.y = ry;
v = this.vertices[4];
v.x = w - rx;
v.y = h;
v.controls.right.clear();
v.controls.left.clear();
// Bottom Left Corner
v = this.vertices[5];
v.x = -(w - rx);
v.y = h;
v.controls.left.clear();
v.controls.right.x = -rx;
v.controls.right.y = 0;
v = this.vertices[6];
v.x = -w;
v.y = h - ry;
v.controls.left.clear();
v.controls.right.clear();
// Upper Left Corner
v = this.vertices[7];
v.x = -w;
v.y = -(h - ry);
v.controls.left.clear();
v.controls.right.x = 0;
v.controls.right.y = -ry;
v = this.vertices[8];
v.x = -(w - rx);
v.y = -h;
v.controls.left.clear();
v.controls.right.clear();
v = this.vertices[9];
v.copy(this.vertices[8]);
}
super._update.call(this);
return this;
}
/**
* @name Two.RoundedRectangle#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagWidth = this._flagHeight = this._flagRadius = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.RoundedRectangle#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.RoundedRectangle}
* @description Create a new instance of {@link Two.RoundedRectangle} with the same properties of the current path.
*/
clone(parent) {
const width = this.width;
const height = this.height;
const radius = this.radius;
const clone = new RoundedRectangle(0, 0, width, height, radius);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.RoundedRectangle#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = 'rounded-rectangle';
for (let i = 0; i < RoundedRectangle.Properties.length; i++) {
const k = RoundedRectangle.Properties[i];
object[k] = this[k];
}
object.radius =
typeof this.radius === 'number' ? this.radius : this.radius.toObject();
return object;
}
}
const proto = {
width: {
enumerable: true,
get: function () {
return this._width;
},
set: function (v) {
this._width = v;
this._flagWidth = true;
},
},
height: {
enumerable: true,
get: function () {
return this._height;
},
set: function (v) {
this._height = v;
this._flagHeight = true;
},
},
radius: {
enumerable: true,
get: function () {
return this._radius;
},
set: function (v) {
if (this._radius instanceof Vector) {
this._radius.unbind(Events.Types.change, this._renderer.flagRadius);
}
this._radius = v;
if (this._radius instanceof Vector) {
this._radius.bind(Events.Types.change, this._renderer.flagRadius);
}
this._flagRadius = true;
},
},
};
/**
* @name FlagRadius
* @private
* @property {Function} - A convenience function to trigger the flag for radius changing.
*/
function FlagRadius() {
this._flagRadius = true;
}
================================================
FILE: src/shapes/star.d.ts
================================================
declare module 'two.js/src/shapes/star' {
/**
* @name Two.Star
* @class
* @param {Number} [x=0] - The x position of the star.
* @param {Number} [y=0] - The y position of the star.
* @param {Number} [innerRadius=0] - The inner radius value of the star.
* @param {Number} [outerRadius=0] - The outer radius value of the star.
* @param {Number} [sides=5] - The number of sides used to construct the star.
*/
export class Star extends Path {
/**
* @name Two.Star.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Star}.
*/
static Properties: ('innerRadius' | 'outerRadius' | 'sides' | string)[];
/**
* @name Two.Star.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Star} to create a new instance
* @returns {Two.Star}
* @description Create a new {@link Two.Star} from an object notation of a {@link Two.Star}.
* @nota-bene Works in conjunction with {@link Two.Star#toObject}
*/
static fromObject(
obj: Parameters[0] & {
innerRadius?: number;
outerRadius?: number;
sides?: number;
}
): Star;
constructor(
ox?: number,
oy?: number,
ir?: number,
or?: number,
sides?: number
);
/**
* @name Two.Star#_flagInnerRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.Star#innerRadius} needs updating.
*/
private _flagInnerRadius;
/**
* @name Two.Star#_flagOuterRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.Star#outerRadius} needs updating.
*/
private _flagOuterRadius;
/**
* @name Two.Star#_flagSides
* @private
* @property {Boolean} - Determines whether the {@link Two.Star#sides} needs updating.
*/
private _flagSides;
/**
* @name Two.Star#_innerRadius
* @private
* @see {@link Two.Star#innerRadius}
*/
private _innerRadius;
/**
* @name Two.Star#_outerRadius
* @private
* @see {@link Two.Star#outerRadius}
*/
private _outerRadius;
/**
* @name Two.Star#_sides
* @private
* @see {@link Two.Star#sides}
*/
private _sides;
/**
* @name Two.Star#innerRadius
* @property {Number} - The size of the inner radius of the star.
*/
innerRadius: number;
/**
* @name Two.Star#outerRadius
* @property {Number} - The size of the outer radius of the star.
*/
outerRadius: number;
/**
* @name Two.Star#sides
* @property {Number} - The amount of sides the star has.
*/
sides: number;
/**
* @name Two.Star#copy
* @function
* @param {Two.Star} star - The reference {@link Two.Star}
* @description Copy the properties of one {@link Two.Star} onto another.
*/
copy(star: Star): Star;
/**
* @name Two.Star#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Star}
* @description Create a new instance of {@link Two.Star} with the same properties of the current path.
*/
clone(parent: Group): Star;
}
import { Path } from 'two.js/src/path';
import { Group } from 'two.js/src/group';
}
================================================
FILE: src/shapes/star.js
================================================
import { Commands } from '../utils/path-commands.js';
import { TWO_PI } from '../utils/math.js';
import { Path } from '../path.js';
import { Anchor } from '../anchor.js';
const cos = Math.cos,
sin = Math.sin;
/**
* @name Two.Star
* @class
* @extends Two.Path
* @param {Number} [x=0] - The x position of the star.
* @param {Number} [y=0] - The y position of the star.
* @param {Number} [innerRadius=0] - The inner radius value of the star.
* @param {Number} [outerRadius=0] - The outer radius value of the star.
* @param {Number} [sides=5] - The number of sides used to construct the star.
*/
export class Star extends Path {
/**
* @name Two.Star#_flagInnerRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.Star#innerRadius} needs updating.
*/
_flagInnerRadius = false;
/**
* @name Two.Star#_flagOuterRadius
* @private
* @property {Boolean} - Determines whether the {@link Two.Star#outerRadius} needs updating.
*/
_flagOuterRadius = false;
/**
* @name Two.Star#_flagSides
* @private
* @property {Boolean} - Determines whether the {@link Two.Star#sides} needs updating.
*/
_flagSides = false;
/**
* @name Two.Star#_innerRadius
* @private
* @see {@link Two.Star#innerRadius}
*/
_innerRadius = 0;
/**
* @name Two.Star#_outerRadius
* @private
* @see {@link Two.Star#outerRadius}
*/
_outerRadius = 0;
/**
* @name Two.Star#_sides
* @private
* @see {@link Two.Star#sides}
*/
_sides = 0;
constructor(x, y, innerRadius, outerRadius, sides) {
if (arguments.length <= 3) {
outerRadius = innerRadius;
innerRadius = outerRadius / 2;
}
if (typeof sides !== 'number' || sides <= 0) {
sides = 5;
}
super();
this._renderer.type = 'star';
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this.closed = true;
this.automatic = false;
/**
* @name Two.Star#innerRadius
* @property {Number} - The size of the inner radius of the star.
*/
if (typeof innerRadius === 'number') {
this.innerRadius = innerRadius;
}
/**
* @name Two.Star#outerRadius
* @property {Number} - The size of the outer radius of the star.
*/
if (typeof outerRadius === 'number') {
this.outerRadius = outerRadius;
}
/**
* @name Two.Star#sides
* @property {Number} - The amount of sides the star has.
*/
if (typeof sides === 'number') {
this.sides = sides;
}
this._update();
if (typeof x === 'number') {
this.translation.x = x;
}
if (typeof y === 'number') {
this.translation.y = y;
}
}
/**
* @name Two.Star.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Star}.
*/
static Properties = ['innerRadius', 'outerRadius', 'sides'];
/**
* @name Two.Star.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Star} to create a new instance
* @returns {Two.Star}
* @description Create a new {@link Two.Star} from an object notation of a {@link Two.Star}.
* @nota-bene Works in conjunction with {@link Two.Star#toObject}
*/
static fromObject(obj) {
const star = new Star().copy(obj);
if ('id' in obj) {
star.id = obj.id;
}
return star;
}
/**
* @name Two.Star#copy
* @function
* @param {Two.Star} star - The reference {@link Two.Star}
* @description Copy the properties of one {@link Two.Star} onto another.
*/
copy(star) {
super.copy.call(this, star);
for (let i = 0; i < Star.Properties.length; i++) {
const k = Star.Properties[i];
if (k in star && typeof star[k] === 'number') {
this[k] = star[k];
}
}
return this;
}
/**
* @name Two.Star#_update
* @function
* @private
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
* @description This is called before rendering happens by the renderer. This applies all changes necessary so that rendering is up-to-date but not updated more than it needs to be.
* @nota-bene Try not to call this method more than once a frame.
*/
_update() {
if (
this._flagVertices ||
this._flagInnerRadius ||
this._flagOuterRadius ||
this._flagSides
) {
const sides = this._sides * 2;
const amount = sides + 1;
let length = this.vertices.length;
if (length > sides) {
this.vertices.splice(sides - 1, length - sides);
length = sides;
}
for (let i = 0; i < amount; i++) {
const pct = (i + 0.5) / sides;
const theta = TWO_PI * pct;
const r = (!(i % 2) ? this._innerRadius : this._outerRadius) / 2;
const x = r * cos(theta);
const y = r * sin(theta);
if (i >= length) {
this.vertices.push(new Anchor(x, y));
} else {
this.vertices[i].set(x, y);
}
this.vertices[i].command = i === 0 ? Commands.move : Commands.line;
}
}
super._update.call(this);
return this;
}
/**
* @name Two.Star#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
this._flagInnerRadius = this._flagOuterRadius = this._flagSides = false;
super.flagReset.call(this);
return this;
}
/**
* @name Two.Star#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Star}
* @description Create a new instance of {@link Two.Star} with the same properties of the current path.
*/
clone(parent) {
const ir = this.innerRadius;
const or = this.outerRadius;
const sides = this.sides;
const clone = new Star(0, 0, ir, or, sides);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
clone.skewX = this.skewX;
clone.skewY = this.skewY;
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
for (let i = 0; i < Path.Properties.length; i++) {
const k = Path.Properties[i];
clone[k] = this[k];
}
if (parent) {
parent.add(clone);
}
return clone;
}
/**
* @name Two.Star#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the path.
*/
toObject() {
const object = super.toObject.call(this);
object.renderer.type = 'star';
for (let i = 0; i < Star.Properties.length; i++) {
const k = Star.Properties[i];
object[k] = this[k];
}
return object;
}
}
const proto = {
innerRadius: {
enumerable: true,
get: function () {
return this._innerRadius;
},
set: function (v) {
this._innerRadius = v;
this._flagInnerRadius = true;
},
},
outerRadius: {
enumerable: true,
get: function () {
return this._outerRadius;
},
set: function (v) {
this._outerRadius = v;
this._flagOuterRadius = true;
},
},
sides: {
enumerable: true,
get: function () {
return this._sides;
},
set: function (v) {
this._sides = v;
this._flagSides = true;
},
},
};
================================================
FILE: src/text.d.ts
================================================
declare module 'two.js/src/text' {
export type AlignmentProperties = 'left' | 'center' | 'right';
export type StyleProperties = 'normal' | 'italic';
export type DecorationProperties = 'underline' | 'strikethrough' | 'none';
export type DirectionProperties = 'ltr' | 'rtl';
export type BaselineProperties = 'top' | 'middle' | 'bottom' | 'baseline';
/**
* @name Two.Text
* @class
* @param {String} [message] - The String to be rendered to the scene.
* @param {Number} [x=0] - The position in the x direction for the object.
* @param {Number} [y=0] - The position in the y direction for the object.
* @param {Object} [styles] - An object where styles are applied. Attribute must exist in Two.Text.Properties.
* @description This is a primitive class for creating drawable text that can be added to the scenegraph.
* @returns {Text}
*/
export class Text extends Shape {
/**
* @name Two.Text.Ratio
* @property {Number} - Approximate aspect ratio of a typeface's character width to height.
*/
static Ratio: number;
/**
* @name Two.Text.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Text}.
*/
static Properties: string[];
/**
* @name Two.Measure
* @function
* @param {Two.Text} [text] - The instance of {@link Two.Text} to measure.
* @returns {Object} - The width and height of the {@link Two.Text} instance.
*/
static Measure(text: Text): Dimensions;
/**
* @name Two.Text.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Text} to create a new instance
* @returns {Two.Text}
* @description Create a new {@link Two.Text} from an object notation of a {@link Two.Text}.
* @nota-bene Works in conjunction with {@link Two.Text#toObject}
*/
static fromObject(
obj: Parameters[0] & {
value?: string;
family?: string;
size?: number;
leading?: number;
alignment?: AlignmentProperties;
linewidth?: number;
style?: StyleProperties;
weight?: number | string;
decoration?: DecorationProperties;
direction?: DirectionProperties;
baseline?: BaselineProperties;
opacity?: number;
visible?: boolean;
fill?: string;
stroke?: string;
dashes?: number[] & {
offset?: number;
};
}
): Text;
constructor(
message?: string,
x?: number,
y?: number,
styles?: {
value?: string;
family?: string;
size?: number;
leading?: number;
alignment?: AlignmentProperties;
linewidth?: number;
style?: StyleProperties;
weight?: number | string;
decoration?: DecorationProperties;
direction?: DirectionProperties;
baseline?: BaselineProperties;
opacity?: number;
visible?: boolean;
fill?: string | Gradient | Texture;
stroke?: string | Gradient | Texture;
dashes?: number[] & { offset?: number };
}
);
/**
* @name Two.Text#_flagValue
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#value} need updating.
*/
private _flagValue;
/**
* @name Two.Text#_flagFamily
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#family} need updating.
*/
private _flagFamily;
/**
* @name Two.Text#_flagSize
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#size} need updating.
*/
private _flagSize;
/**
* @name Two.Text#_flagLeading
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#leading} need updating.
*/
private _flagLeading;
/**
* @name Two.Text#_flagAlignment
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#alignment} need updating.
*/
private _flagAlignment;
/**
* @name Two.Text#_flagBaseline
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#baseline} need updating.
*/
private _flagBaseline;
/**
* @name Two.Text#_flagStyle
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#style} need updating.
*/
private _flagStyle;
/**
* @name Two.Text#_flagWeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#weight} need updating.
*/
private _flagWeight;
/**
* @name Two.Text#_flagDecoration
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#decoration} need updating.
*/
private _flagDecoration;
/**
* @name Two.Text#_flagDirection
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#direction} needs updating.
*/
private _flagDirection;
/**
* @name Two.Text#_flagFill
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#fill} need updating.
*/
private _flagFill;
/**
* @name Two.Text#_flagStroke
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#stroke} need updating.
*/
private _flagStroke;
/**
* @name Two.Text#_flagLinewidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#linewidth} need updating.
*/
private _flagLinewidth;
/**
* @name Two.Text#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#opacity} need updating.
*/
private _flagOpacity;
/**
* @name Two.Text#_flagVisible
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#visible} need updating.
*/
private _flagVisible;
/**
* @name Two.Path#_flagMask
* @private
* @property {Boolean} - Determines whether the {@link Two.Path#mask} needs updating.
*/
private _flagMask;
/**
* @name Two.Text#_flagClip
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#clip} need updating.
*/
private _flagClip;
/**
* @name Two.Text#value
* @property {String} - The characters to be rendered to the the screen. Referred to in the documentation sometimes as the `message`.
*/
/**
* @name Two.Text#_dashes
* @private
* @see {@link Two.Text#dashes}
*/
private _dashes;
/**
* @name Two.Path#_strokeAttenuation
* @private
* @see {@link Two.Path#strokeAttenuation}
*/
private _strokeAttenuation;
/**
* @name Two.Text#value
* @property {String} - The characters to be rendered to the the screen. Referred to in the documentation sometimes as the `message`.
*/
value: string;
/**
* @name Two.Text#family
* @property {String} - The font family Two.js should attempt to regsiter for rendering. The default value is `'sans-serif'`. Comma separated font names can be supplied as a "stack", similar to the CSS implementation of `font-family`.
*/
family: string;
/**
* @name Two.Text#size
* @property {Number} - The font size in Two.js point space. Defaults to `13`.
*/
size: number;
/**
* @name Two.Text#leading
* @property {Number} - The height between lines measured from base to base in Two.js point space. Defaults to `17`.
*/
leading: number;
/**
* @name Two.Text#alignment
* @property {String} - Alignment of text in relation to {@link Two.Text#translation}'s coordinates. Possible values include `'left'`, `'center'`, `'right'`. Defaults to `'center'`.
*/
alignment: AlignmentProperties;
/**
* @name Two.Text#baseline
* @property {String} - The vertical aligment of the text in relation to {@link Two.Text#translation}'s coordinates. Possible values include `'top'`, `'middle'`, `'bottom'`, and `'baseline'`. Defaults to `'baseline'`.
*/
baseline: BaselineProperties;
/**
* @name Two.Text#style
* @property {String} - The font's style. Possible values include '`normal`', `'italic'`. Defaults to `'normal'`.
*/
style: StyleProperties;
/**
* @name Two.Text#weight
* @property {Number} - A number at intervals of 100 to describe the font's weight. This compatibility varies with the typeface's variant weights. Larger values are bolder. Smaller values are thinner. Defaults to `500`.
*/
weight: number | string;
/**
* @name Two.Text#decoration
* @property {String} - String to delineate whether text should be decorated with for instance an `'underline'`. Defaults to `'none'`.
*/
decoration: DecorationProperties;
/**
* @name Two.Text#direction
* @property {String} - String to determine what direction the text should run. Possibly values are `'ltr'` for left-to-right and `'rtl'` for right-to-left. Defaults to `'ltr'`.
*/
direction: DirectionProperties;
/**
* @name Two.Text#fill
* @property {(String|Gradient|Texture)} - The value of what the text object should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
fill: string | Gradient | Texture;
/**
* @name Two.Text#stroke
* @property {(String|Gradient|Texture)} - The value of what the text object should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
stroke: string | Gradient | Texture;
/**
* @name Two.Text#linewidth
* @property {Number} - The thickness in pixels of the stroke.
*/
linewidth: number;
/**
* @name Two.Text#opacity
* @property {Number} - The opaqueness of the text object.
* @nota-bene Can be used in conjunction with CSS Colors that have an alpha value.
*/
opacity: number;
/**
* @name Two.Text#visible
* @property {Boolean} - Display the text object or not.
* @nota-bene For {@link Two.CanvasRenderer} and {@link Two.WebGLRenderer} when set to false all updating is disabled improving performance dramatically with many objects in the scene.
*/
visible: boolean;
/**
* @name Two.Text#mask
* @property {Shape} - The shape whose alpha property becomes a clipping area for the text.
* @nota-bene This property is currently not working becuase of SVG spec issues found here {@link https://code.google.com/p/chromium/issues/detail?id=370951}.
*/
mask: Shape | null | undefined;
/**
* @name Two.Text#clip
* @property {Shape} - Object to define clipping area.
* @nota-bene This property is currently not working becuase of SVG spec issues found here {@link https://code.google.com/p/chromium/issues/detail?id=370951}.
*/
clip: boolean;
/**
* @name Two.Text#dashes
* @type {number[] & { offset?: number }}
* @property {Number[]} - Array of numbers. Odd indices represent dash length. Even indices represent dash space.
* @description A list of numbers that represent the repeated dash length and dash space applied to the stroke of the text.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray} for more information on the SVG stroke-dasharray attribute.
*/
dashes: number[] & {
offset?: number;
};
/**
* @name Two.Text#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
* @description When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
*/
strokeAttenuation: boolean;
/**
* @name Two.Text#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the text object.
*/
toObject(): object;
/**
* @name Two.Text#dispose
* @function
* @description Release the text's renderer resources and detach all events.
* This method disposes fill and stroke effects (calling dispose() on
* Gradients and Textures for thorough cleanup) while preserving the
* renderer type for potential re-attachment to a new renderer.
*/
dispose(): Text;
/**
* @name Two.Text#noFill
* @function
* @description Short hand method to set fill to `none`.
*/
noFill(): Text;
/**
* @name Two.Text#noStroke
* @function
* @description Short hand method to set stroke to `none`.
*/
noStroke(): Text;
/**
* @name Two.Text#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the text object.
*/
getBoundingClientRect(shallow?: boolean): BoundingBox;
/**
* @name Two.Text#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset(): Text;
}
import { Shape } from 'two.js/src/shape';
import { Gradient } from 'two.js/src/effects/gradient';
import { Texture } from 'two.js/src/effects/texture';
import { BoundingBox, Dimensions } from 'two.js';
}
================================================
FILE: src/text.js
================================================
import { Events } from './events.js';
import { _ } from './utils/underscore.js';
import { Shape } from './shape.js';
import { Gradient } from './effects/gradient.js';
import { LinearGradient } from './effects/linear-gradient.js';
import { RadialGradient } from './effects/radial-gradient.js';
import { Texture } from './effects/texture.js';
import { root } from './utils/root.js';
import { getEffectFromObject } from './utils/shape.js';
let canvas;
const min = Math.min,
max = Math.max;
if (root.document) {
canvas = document.createElement('canvas');
}
/**
* @name Two.Text
* @class
* @extends Two.Shape
* @param {String} [message] - The String to be rendered to the scene.
* @param {Number} [x=0] - The position in the x direction for the object.
* @param {Number} [y=0] - The position in the y direction for the object.
* @param {Object} [styles] - An object where styles are applied. Attribute must exist in Two.Text.Properties.
* @description This is a primitive class for creating drawable text that can be added to the scenegraph.
* @returns {Two.Text}
*/
export class Text extends Shape {
/**
* @name Two.Text#_flagValue
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#value} need updating.
*/
_flagValue = true;
/**
* @name Two.Text#_flagFamily
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#family} need updating.
*/
_flagFamily = true;
/**
* @name Two.Text#_flagSize
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#size} need updating.
*/
_flagSize = true;
/**
* @name Two.Text#_flagLeading
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#leading} need updating.
*/
_flagLeading = true;
/**
* @name Two.Text#_flagAlignment
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#alignment} need updating.
*/
_flagAlignment = true;
/**
* @name Two.Text#_flagBaseline
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#baseline} need updating.
*/
_flagBaseline = true;
/**
* @name Two.Text#_flagStyle
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#style} need updating.
*/
_flagStyle = true;
/**
* @name Two.Text#_flagWeight
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#weight} need updating.
*/
_flagWeight = true;
/**
* @name Two.Text#_flagDecoration
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#decoration} need updating.
*/
_flagDecoration = true;
/**
* @name Two.Text#_flagFill
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#fill} need updating.
*/
_flagFill = true;
/**
* @name Two.Text#_flagStroke
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#stroke} need updating.
*/
_flagStroke = true;
/**
* @name Two.Text#_flagLinewidth
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#linewidth} need updating.
*/
_flagLinewidth = true;
/**
* @name Two.Text#_flagOpacity
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#opacity} need updating.
*/
_flagOpacity = true;
/**
* @name Two.Text#_flagVisible
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#visible} need updating.
*/
_flagVisible = true;
/**
* @name Two.Text#_flagMask
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#mask} needs updating.
*/
_flagMask = false;
/**
* @name Two.Text#_flagClip
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#clip} needs updating.
*/
_flagClip = false;
/**
* @name Two.Text#_flagDirection
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#direction} needs updating.
*/
_flagDirection = true;
/**
* @name Two.Text#_flagStrokeAttenuation
* @private
* @property {Boolean} - Determines whether the {@link Two.Text#strokeAttenuation} needs updating.
*/
_flagStrokeAttenuation = true;
// Underlying Properties
/**
* @name Two.Text#value
* @property {String} - The characters to be rendered to the the screen. Referred to in the documentation sometimes as the `message`.
*/
_value = '';
/**
* @name Two.Text#family
* @property {String} - The font family Two.js should attempt to register for rendering. The default value is `'sans-serif'`. Comma separated font names can be supplied as a "stack", similar to the CSS implementation of `font-family`.
*/
_family = 'sans-serif';
/**
* @name Two.Text#size
* @property {Number} - The font size in Two.js point space. Defaults to `13`.
*/
_size = 13;
/**
* @name Two.Text#leading
* @property {Number} - The height between lines measured from base to base in Two.js point space. Defaults to `17`.
*/
_leading = 17;
/**
* @name Two.Text#alignment
* @property {String} - Alignment of text in relation to {@link Two.Text#translation}'s coordinates. Possible values include `'left'`, `'center'`, `'right'`. Defaults to `'center'`.
*/
_alignment = 'center';
/**
* @name Two.Text#baseline
* @property {String} - The vertical aligment of the text in relation to {@link Two.Text#translation}'s coordinates. Possible values include `'top'`, `'middle'`, `'bottom'`, and `'baseline'`. Defaults to `'baseline'`.
* @nota-bene In headless environments where the canvas is based on {@link https://github.com/Automattic/node-canvas}, `baseline` seems to be the only valid property.
*/
_baseline = 'middle';
/**
* @name Two.Text#style
* @property {String} - The font's style. Possible values include '`normal`', `'italic'`. Defaults to `'normal'`.
*/
_style = 'normal';
/**
* @name Two.Text#weight
* @property {Number} - A number at intervals of 100 to describe the font's weight. This compatibility varies with the typeface's variant weights. Larger values are bolder. Smaller values are thinner. Defaults to `'500'`.
*/
_weight = 500;
/**
* @name Two.Text#decoration
* @property {String} - String to delineate whether text should be decorated with for instance an `'underline'`. Defaults to `'none'`.
*/
_decoration = 'none';
/**
* @name Two.Text#direction
* @property {String} - String to determine what direction the text should run. Possibly values are `'ltr'` for left-to-right and `'rtl'` for right-to-left. Defaults to `'ltr'`.
*/
_direction = 'ltr';
/**
* @name Two.Text#fill
* @property {(String|Two.Gradient|Two.Texture)} - The value of what the text object should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
_fill = '#000';
/**
* @name Two.Text#stroke
* @property {(String|Two.Gradient|Two.Texture)} - The value of what the text object should be filled in with.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for more information on CSS's colors as `String`.
*/
_stroke = 'none';
/**
* @name Two.Text#linewidth
* @property {Number} - The thickness in pixels of the stroke.
*/
_linewidth = 1;
/**
* @name Two.Text#opacity
* @property {Number} - The opaqueness of the text object.
* @nota-bene Can be used in conjunction with CSS Colors that have an alpha value.
*/
_opacity = 1;
/**
* @name Two.Text#visible
* @property {Boolean} - Display the text object or not.
* @nota-bene For {@link Two.CanvasRenderer} and {@link Two.WebGLRenderer} when set to false all updating is disabled improving performance dramatically with many objects in the scene.
*/
_visible = true;
/**
* @name Two.Text#mask
* @property {Two.Shape} - The shape whose alpha property becomes a clipping area for the text.
* @nota-bene This property is currently not working because of SVG spec issues found here {@link https://code.google.com/p/chromium/issues/detail?id=370951}.
*/
_mask = null;
/**
* @name Two.Text#clip
* @property {Two.Shape} - Object to define clipping area.
* @nota-bene This property is currently not working because of SVG spec issues found here {@link https://code.google.com/p/chromium/issues/detail?id=370951}.
*/
_clip = false;
/**
* @name Two.Text#_dashes
* @private
* @see {@link Two.Text#dashes}
*/
_dashes = null;
/**
* @name Two.Text#_strokeAttenuation
* @private
* @see {@link Two.Text#strokeAttenuation}
*/
_strokeAttenuation = true;
constructor(message, x, y, styles) {
super();
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
this._renderer.type = 'text';
this._renderer.flagFill = FlagFill.bind(this);
this._renderer.flagStroke = FlagStroke.bind(this);
this.value = message;
if (typeof x === 'number') {
this.translation.x = x;
}
if (typeof y === 'number') {
this.translation.y = y;
}
/**
* @name Two.Text#dashes
* @property {Number[]} - Array of numbers. Odd indices represent dash length. Even indices represent dash space.
* @description A list of numbers that represent the repeated dash length and dash space applied to the stroke of the text.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray} for more information on the SVG stroke-dasharray attribute.
*/
this.dashes = [];
/**
* @name Two.Text#dashes#offset
* @property {Number} - A number in pixels to offset {@link Two.Text#dashes} display.
*/
this.dashes.offset = 0;
if (!_.isObject(styles)) {
return this;
}
for (let i = 0; i < Text.Properties.length; i++) {
const property = Text.Properties[i];
if (property in styles) {
this[property] = styles[property];
}
}
}
/**
* @name Two.Text.Ratio
* @property {Number} - Approximate aspect ratio of a typeface's character width to height.
*/
static Ratio = 0.6;
/**
* @name Two.Text.Properties
* @property {String[]} - A list of properties that are on every {@link Two.Text}.
*/
static Properties = [
'value',
'family',
'size',
'leading',
'alignment',
'linewidth',
'style',
'weight',
'decoration',
'direction',
'baseline',
'opacity',
'visible',
'fill',
'stroke',
'dashes',
'strokeAttenuation',
];
/**
*
* @name Two.Measure
* @function
* @param {Two.Text} [text] - The instance of {@link Two.Text} to measure.
* @returns {Object} - The width and height of the {@link Two.Text} instance.
*/
static Measure(text) {
if (canvas) {
const ctx = canvas.getContext('2d');
ctx.font = [
text._style,
text._weight,
`${text._size}px/${text._leading}px`,
text._family,
].join(' ');
const metrics = ctx.measureText(text.value, 0, 0);
const height =
metrics.actualBoundingBoxDescent + metrics.actualBoundingBoxAscent;
return {
width: metrics.width,
height,
};
} else {
const width = this.value.length * this.size * Text.Ratio;
const height = this.leading;
console.warn(
'Two.Text: unable to accurately measure text, so using an approximation.'
);
return {
width,
height,
};
}
}
/**
* @name Two.Text.fromObject
* @function
* @param {Object} obj - Object notation of a {@link Two.Text} to create a new instance
* @returns {Two.Text}
* @description Create a new {@link Two.Text} from an object notation of a {@link Two.Text}.
* @nota-bene Works in conjunction with {@link Two.Text#toObject}
*/
static fromObject(obj) {
const fill =
typeof obj.fill === 'string' ? obj.fill : getEffectFromObject(obj.fill);
const stroke =
typeof obj.stroke === 'string'
? obj.stroke
: getEffectFromObject(obj.stroke);
const text = new Text().copy({ ...obj, fill, stroke });
if ('id' in obj) {
text.id = obj.id;
}
return text;
}
/**
* @name Two.Text#copy
* @function
* @param {Two.Text} text
* @description Copy the properties of one {@link Two.Text} onto another.
*/
copy(text) {
super.copy.call(this, text);
for (let i = 0; i < Text.Properties.length; i++) {
const k = Text.Properties[i];
if (k in text) {
this[k] = text[k];
}
}
return this;
}
/**
* @name Two.Text#clone
* @function
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
* @returns {Two.Text}
* @description Create a new instance of {@link Two.Text} with the same properties of the current text object.
*/
clone(parent) {
const clone = new Text(this.value);
clone.translation.copy(this.translation);
clone.rotation = this.rotation;
clone.scale = this.scale;
for (let i = 0; i < Text.Properties.length; i++) {
const prop = Text.Properties[i];
clone[prop] = this[prop];
}
if (this.matrix.manual) {
clone.matrix.copy(this.matrix);
}
if (parent) {
parent.add(clone);
}
return clone._update();
}
/**
* @name Two.Text#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the text object.
* @nota-bene Works in conjunction with {@link Two.Text.fromObject}
*/
toObject() {
const result = super.toObject.call(this);
result.renderer.type = 'text';
for (let i = 0; i < Text.Properties.length; i++) {
const prop = Text.Properties[i];
result[prop] = this[prop];
}
return result;
}
/**
* @name Two.Text#dispose
* @function
* @returns {Two.Text}
* @description Release the text's renderer resources and detach all events.
* This method disposes fill and stroke effects (calling dispose() on
* Gradients and Textures for thorough cleanup) while preserving the
* renderer type for potential re-attachment to a new renderer.
*/
dispose() {
// Call parent dispose to preserve renderer type and unbind events
super.dispose();
// Dispose fill effect (more thorough than unbind)
if (
typeof this.fill === 'object' &&
typeof this.fill.dispose === 'function'
) {
this.fill.dispose();
} else if (
typeof this.fill === 'object' &&
typeof this.fill.unbind === 'function'
) {
this.fill.unbind();
}
// Dispose stroke effect (more thorough than unbind)
if (
typeof this.stroke === 'object' &&
typeof this.stroke.dispose === 'function'
) {
this.stroke.dispose();
} else if (
typeof this.stroke === 'object' &&
typeof this.stroke.unbind === 'function'
) {
this.stroke.unbind();
}
return this;
}
/**
* @name Two.Text#noFill
* @function
* @description Short hand method to set fill to `none`.
*/
noFill() {
this.fill = 'none';
return this;
}
/**
* @name Two.Text#noStroke
* @function
* @description Short hand method to set stroke to `none`.
*/
noStroke() {
this.stroke = 'none';
this.linewidth = 0;
return this;
}
// A shim to not break `getBoundingClientRect` calls.
// TODO: Implement a way to calculate proper bounding
// boxes of `Two.Text`.
/**
* @name Two.Text#getBoundingClientRect
* @function
* @param {Boolean} [shallow=false] - Describes whether to calculate off local matrix or world matrix.
* @returns {Object} - Returns object with top, left, right, bottom, width, height attributes.
* @description Return an object with top, left, right, bottom, width, and height parameters of the text object.
*/
getBoundingClientRect(shallow) {
let matrix;
let left, right, top, bottom;
// TODO: Update this to not __always__ update. Just when it needs to.
this._update(true);
matrix = shallow ? this.matrix : this.worldMatrix;
const { width, height } = Text.Measure(this);
const border = (this._linewidth || 0) / 2;
switch (this.alignment) {
case 'left':
left = -border;
right = width + border;
break;
case 'right':
left = -(width + border);
right = border;
break;
default:
left = -(width / 2 + border);
right = width / 2 + border;
}
switch (this.baseline) {
case 'middle':
top = -(height / 2 + border);
bottom = height / 2 + border;
break;
default:
top = -(height + border);
bottom = border;
}
const [ax, ay] = matrix.multiply(left, top);
const [bx, by] = matrix.multiply(left, bottom);
const [cx, cy] = matrix.multiply(right, top);
const [dx, dy] = matrix.multiply(right, bottom);
top = min(ay, by, cy, dy);
left = min(ax, bx, cx, dx);
right = max(ax, bx, cx, dx);
bottom = max(ay, by, cy, dy);
return {
top: top,
left: left,
right: right,
bottom: bottom,
width: right - left,
height: bottom - top,
};
}
/**
* @name Two.Text#flagReset
* @function
* @private
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
*/
flagReset() {
super.flagReset.call(this);
this._flagValue =
this._flagFamily =
this._flagSize =
this._flagLeading =
this._flagAlignment =
this._flagFill =
this._flagStroke =
this._flagLinewidth =
this._flagOpacity =
this._flagVisible =
this._flagClip =
this._flagDecoration =
this._flagClassName =
this._flagBaseline =
this._flagWeight =
this._flagStyle =
this._flagDirection =
false;
return this;
}
}
const proto = {
value: {
enumerable: true,
get: function () {
return this._value;
},
set: function (v) {
this._value = v;
this._flagValue = true;
},
},
family: {
enumerable: true,
get: function () {
return this._family;
},
set: function (v) {
this._family = v;
this._flagFamily = true;
},
},
size: {
enumerable: true,
get: function () {
return this._size;
},
set: function (v) {
this._size = v;
this._flagSize = true;
},
},
leading: {
enumerable: true,
get: function () {
return this._leading;
},
set: function (v) {
this._leading = v;
this._flagLeading = true;
},
},
alignment: {
enumerable: true,
get: function () {
return this._alignment;
},
set: function (v) {
this._alignment = v;
this._flagAlignment = true;
},
},
linewidth: {
enumerable: true,
get: function () {
return this._linewidth;
},
set: function (v) {
this._linewidth = v;
this._flagLinewidth = true;
},
},
style: {
enumerable: true,
get: function () {
return this._style;
},
set: function (v) {
this._style = v;
this._flagStyle = true;
},
},
weight: {
enumerable: true,
get: function () {
return this._weight;
},
set: function (v) {
this._weight = v;
this._flagWeight = true;
},
},
decoration: {
enumerable: true,
get: function () {
return this._decoration;
},
set: function (v) {
this._decoration = v;
this._flagDecoration = true;
},
},
direction: {
enumerable: true,
get: function () {
return this._direction;
},
set: function (v) {
this._direction = v;
this._flagDirection = true;
},
},
baseline: {
enumerable: true,
get: function () {
return this._baseline;
},
set: function (v) {
this._baseline = v;
this._flagBaseline = true;
},
},
opacity: {
enumerable: true,
get: function () {
return this._opacity;
},
set: function (v) {
this._opacity = v;
this._flagOpacity = true;
},
},
visible: {
enumerable: true,
get: function () {
return this._visible;
},
set: function (v) {
this._visible = v;
this._flagVisible = true;
},
},
fill: {
enumerable: true,
get: function () {
return this._fill;
},
set: function (f) {
if (
this._fill instanceof Gradient ||
this._fill instanceof LinearGradient ||
this._fill instanceof RadialGradient ||
this._fill instanceof Texture
) {
this._fill.unbind(Events.Types.change, this._renderer.flagFill);
}
this._fill = f;
this._flagFill = true;
if (
this._fill instanceof Gradient ||
this._fill instanceof LinearGradient ||
this._fill instanceof RadialGradient ||
this._fill instanceof Texture
) {
this._fill.bind(Events.Types.change, this._renderer.flagFill);
}
},
},
stroke: {
enumerable: true,
get: function () {
return this._stroke;
},
set: function (f) {
if (
this._stroke instanceof Gradient ||
this._stroke instanceof LinearGradient ||
this._stroke instanceof RadialGradient ||
this._stroke instanceof Texture
) {
this._stroke.unbind(Events.Types.change, this._renderer.flagStroke);
}
this._stroke = f;
this._flagStroke = true;
if (
this._stroke instanceof Gradient ||
this._stroke instanceof LinearGradient ||
this._stroke instanceof RadialGradient ||
this._stroke instanceof Texture
) {
this._stroke.bind(Events.Types.change, this._renderer.flagStroke);
}
},
},
mask: {
enumerable: true,
get: function () {
return this._mask;
},
set: function (v) {
this._mask = v;
this._flagMask = true;
if (_.isObject(v) && !v.clip) {
v.clip = true;
}
},
},
clip: {
enumerable: true,
get: function () {
return this._clip;
},
set: function (v) {
this._clip = v;
this._flagClip = true;
},
},
dashes: {
enumerable: true,
get: function () {
return this._dashes;
},
set: function (v) {
if (typeof v.offset !== 'number') {
v.offset = (this.dashes && this._dashes.offset) || 0;
}
this._dashes = v;
},
},
/**
* @name Two.Text#strokeAttenuation
* @property {Boolean} - When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
* @description When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
*/
strokeAttenuation: {
enumerable: true,
get: function () {
return this._strokeAttenuation;
},
set: function (v) {
this._strokeAttenuation = !!v;
this._flagStrokeAttenuation = true;
this._flagLinewidth = true;
},
},
};
/**
* @name Two.Text.FlagFill
* @function
* @private
* @description Cached method to let renderers know the fill property have been updated on a {@link Two.Text}.
*/
function FlagFill() {
this._flagFill = true;
}
/**
* @name Two.Text.FlagStroke
* @function
* @private
* @description Cached method to let renderers know the stroke property have been updated on a {@link Two.Text}.
*/
function FlagStroke() {
this._flagStroke = true;
}
================================================
FILE: src/two.d.ts
================================================
declare module 'two.js' {
/**
* @name Two
* @class
* @global
* @param {Object} [options]
* @param {Boolean} [options.fullscreen=false] - Set to `true` to automatically make the stage adapt to the width and height of the parent document. This parameter overrides `width` and `height` parameters if set to `true`. This overrides `options.fitted` as well.
* @param {Boolean} [options.fitted=false] = Set to `true` to automatically make the stage adapt to the width and height of the parent element. This parameter overrides `width` and `height` parameters if set to `true`.
* @param {Number} [options.width=640] - The width of the stage on construction. This can be set at a later time.
* @param {Number} [options.height=480] - The height of the stage on construction. This can be set at a later time.
* @param {String} [options.type=Two.Types.svg] - The type of renderer to setup drawing with. See {@link Two.Types} for available options.
* @param {Boolean} [options.autostart=false] - Set to `true` to add the instance to draw on `requestAnimationFrame`. This is a convenient substitute for {@link Two#play}.
* @param {Element} [options.domElement] - The canvas or SVG element to draw into. This overrides the `options.type` argument.
* @description The entrypoint for Two.js. Instantiate a `new Two` in order to setup a scene to render to. `Two` is also the publicly accessible interface that all other sub-classes, functions, and utilities attach to.
*/
export default class Two {
static NextFrameId: any;
/**
* @name Two.Types
* @property {Object} - The different rendering types available in the library.
*/
static Types: {
webgl: 'WebGLRenderer';
svg: 'SVGRenderer';
canvas: 'CanvasRenderer';
};
/**
* @name Two.Version
* @property {String} - The current working version of the library, `$version`.
*/
static Version: string;
/**
* @name Two.PublishDate
* @property {String} - The automatically generated publish date in the build process to verify version release candidates.
*/
static PublishDate: string;
/**
* @name Two.Identifier
* @property {String} - String prefix for all Two.js object's ids. This trickles down to SVG ids.
*/
static Identifier: string;
/**
* @name Two.Resolution
* @property {Number} - Default amount of vertices to be used for interpreting Arcs and ArcSegments.
*/
static Resolution: number;
/**
* @name Two.AutoCalculateImportedMatrices
* @property {Boolean} - When importing SVGs through the {@link Two#interpret} and {@link Two#load}, this boolean determines whether Two.js infers and then overrides the exact transformation matrix of the reference SVG.
* @nota-bene `false` copies the exact transformation matrix values, but also sets the path's `matrix.manual = true`.
*/
static AutoCalculateImportedMatrices: boolean;
/**
* @name Two.Instances
* @property {Two[]} - Registered list of all Two.js instances in the current session.
*/
static Instances: Two[];
/**
* @function Two.uniqueId
* @description Simple method to access an incrementing value. Used for `id` allocation on all Two.js objects.
* @returns {Number} Ever increasing Number.
*/
static uniqueId: () => number;
static Anchor: typeof Anchor;
static Collection: typeof Collection;
static Events: typeof Events;
static Group: typeof Group;
static Matrix: typeof Matrix;
static Path: typeof Path;
static Registry: typeof Registry;
static Element: typeof TwoElement;
static Shape: typeof Shape;
static Text: typeof Text;
static Vector: typeof Vector;
static Gradient: typeof Gradient;
static Image: typeof Image;
static ImageSequence: typeof ImageSequence;
static LinearGradient: typeof LinearGradient;
static RadialGradient: typeof RadialGradient;
static Sprite: typeof Sprite;
static Stop: typeof Stop;
static Texture: typeof Texture;
static ArcSegment: typeof ArcSegment;
static Circle: typeof Circle;
static Ellipse: typeof Ellipse;
static Line: typeof Line;
static Points: typeof Points;
static Polygon: typeof Polygon;
static Rectangle: typeof Rectangle;
static RoundedRectangle: typeof RoundedRectangle;
static Star: typeof Star;
static CanvasRenderer: typeof CanvasRenderer;
static SVGRenderer: typeof SVGRenderer;
static WebGLRenderer: typeof WebGLRenderer;
static Commands: {
move: 'M';
line: 'L';
curve: 'C';
arc: 'A';
close: 'Z';
};
/**
* @name Two.Utils
* @property {Object} - A massive object filled with utility functions and properties.
*/
static Utils: any;
constructor(options?: {
fullscreen?: boolean;
fitted?: boolean;
autostart?: boolean;
width?: number;
height?: number;
type?: (typeof Two.Types)[keyof typeof Two.Types];
domElement?: SVGElement | HTMLCanvasElement;
overdraw?: boolean;
smoothing?: boolean;
ratio?: number;
});
/**
* @private
*/
private _events;
set _bound(arg: boolean);
get _bound(): boolean;
addEventListener(...args: any[]): any;
on(...args: any[]): any;
bind(...args: any[]): any;
removeEventListener(...args: any[]): any;
off(...args: any[]): any;
unbind(...args: any[]): any;
dispatchEvent(...args: any[]): any;
trigger(...args: any[]): any;
listen(...args: any[]): any;
ignore(...args: any[]): any;
/**
* @name Two#type
* @property {String} - A string representing which type of renderer the instance has instantiated.
*/
type: (typeof Two.Types)[keyof typeof Two.Types];
/**
* @name Two#renderer
* @property {(Two.SVGRenderer|CanvasRenderer|WebGLRenderer)} - The instantiated rendering class for the instance. For a list of possible rendering types check out Two.Types.
*/
renderer: SVGRenderer | CanvasRenderer | WebGLRenderer;
/**
* @name Two#scene
* @property {Group} - The base level {@link Two.Group} which houses all objects for the instance. Because it is a {@link Two.Group} transformations can be applied to it that will affect all objects in the instance. This is handy as a makeshift inverted camera.
*/
scene: Group;
/**
* @name Two#width
* @property {Number} - The width of the instance's dom element.
*/
width: number;
/**
* @name Two#height
* @property {Number} - The height of the instance's dom element.
*/
height: number;
/**
* @name Two#frameCount
* @property {Number} - An integer representing how many frames have elapsed.
*/
frameCount: number;
/**
* @name Two#timeDelta
* @property {Number} - A number representing how much time has elapsed since the last frame in milliseconds.
*/
timeDelta: number;
/**
* @name Two#playing
* @property {Boolean} - A boolean representing whether or not the instance is being updated through the automatic `requestAnimationFrame`.
*/
playing: boolean;
fit(): void;
/**
* @name Two#appendTo
* @function
* @param {Element} elem - The DOM element to append the Two.js stage to.
* @description Shorthand method to append your instance of Two.js to the `document`.
*/
appendTo(elem: HTMLElement): Two;
/**
* @name Two#play
* @function
* @fires Two.Events.Types.play event
* @description Call to start an internal animation loop.
* @nota-bene This function initiates a `requestAnimationFrame` loop.
*/
play(): Two;
/**
* @name Two#pause
* @function
* @fires Two.Events.Types.pause event
* @description Call to stop the internal animation loop for a specific instance of Two.js.
*/
pause(): Two;
setPlaying(p: boolean): Two;
/**
* @name Two#release
* @function
* @param {Two.Element} [obj] - Object to release from event listening. If none provided then the root {@link Two.Group} will be used.
* @returns {Two.Element} The object passed for event deallocation.
* @description Release a {@link Two.Element}’s events from memory and recurse through its children, effects, and/or vertices.
*/
release(obj?: TwoElement): T;
getShapesAtPoint(
x: number,
y: number,
options?: SceneHitTestOptions
): Shape[];
/**
* @name Two#update
* @function
* @fires Two.Events.Types.update event
* @description Update positions and calculations in one pass before rendering. Then render to the canvas.
* @nota-bene This function is called automatically if using {@link Two#play} or the `autostart` parameter in construction.
*/
update(): Two;
_lastFrame: number;
/**
* @name Two#render
* @function
* @fires render
* @description Render all drawable and visible objects of the scene.
*/
render(): Two;
/**
* @name Two#add
* @function
* @param {Shape | Shape[]} [objects] - An array of Two.js objects. Alternatively can add objects as individual arguments.
* @description A shorthand method to add specific Two.js objects to the scene.
*/
add(objects: Shape[]): Two;
/**
* @name Two#add
* @function
* @param {...Shape} [args] - Alternatively pass each shape as an argument
* @description A shorthand method to add specific Two.js objects to the scene.
*/
add(...args: Shape[]): Two;
/**
* @name Two#remove
* @function
* @param {Shape | Shape[]} [objects] - An array of Two.js objects.
* @description A shorthand method to remove specific Two.js objects from the scene.
*/
remove(objects: Shape[]): Two;
/**
* @name Two#remove
* @function
* @param {...Shape} [args] - Alternatively pass each shape as an argument
* @description A shorthand method to remove specific Two.js objects from the scene.
*/
remove(...args: Shape[]): Two;
/**
* @name Two#clear
* @function
* @description Removes all objects from the instance's scene. If you intend to have the browser garbage collect this, don't forget to delete the references in your application as well.
*/
clear(): Two;
/**
* @name Two#makeLine
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* @returns {Line}
* @description Creates a Two.js line and adds it to the scene.
*/
makeLine(x1: number, y1: number, x2: number, y2: number): Line;
/**
* @name Two#makeArrow
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* @returns {Path}
* @description Creates a Two.js arrow and adds it to the scene.
*/
makeArrow(
x1: number,
y1: number,
x2: number,
y2: number,
size?: number
): Path;
/**
* @name Two#makeRectangle
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} width
* @param {Number} height
* @returns {Rectangle}
* @description Creates a Two.js rectangle and adds it to the scene.
*/
makeRectangle(
x: number,
y: number,
width: number,
height: number
): Rectangle;
/**
* @name Two#makeRoundedRectangle
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} width
* @param {Number} height
* @param {Number} radius
* @returns {RoundedRectangle}
* @description Creates a Two.js rounded rectangle and adds it to the scene.
*/
makeRoundedRectangle(
x: number,
y: number,
width: number,
height: number,
radius: number | Vector
): RoundedRectangle;
/**
* @name Two#makeCircle
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} radius
* @param {Number} [resolution=4]
* @returns {Circle}
* @description Creates a Two.js circle and adds it to the scene.
*/
makeCircle(
x: number,
y: number,
radius: number,
resolution?: number
): Circle;
/**
* @name Two#makeEllipse
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} rx
* @param {Number} ry
* @param {Number} [resolution=4]
* @returns {Ellipse}
* @description Creates a Two.js ellipse and adds it to the scene.
*/
makeEllipse(
x: number,
y: number,
rx: number,
ry: number,
resolution?: number
): Ellipse;
/**
* @name Two#makeStar
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} outerRadius
* @param {Number} innerRadius
* @param {Number} sides
* @returns {Star}
* @description Creates a Two.js star and adds it to the scene.
*/
makeStar(
x: any,
y: any,
outerRadius: number,
innerRadius: number,
sides: number
): Star;
/**
* @name Two#makeCurve
* @function
* @param {Anchor[]} [points] - An array of {@link Two.Anchor} points.
* @returns {Path} - Where `path.curved` is set to `true`.
* @description Creates a Two.js path that is curved and adds it to the scene.
* @nota-bene In either case of passing an array or passing numbered arguments the last argument is an optional `Boolean` that defines whether the path should be open or closed.
*/
makeCurve(points?: Anchor[]): Path;
/**
* @name Two#makeCurve
* @function
* @param {...Number} [args] - Alternatively you can pass alternating `x` / `y` coordinate values as individual arguments. These will be combined into {@link Two.Anchor}s for use in the path.
* @returns {Path} - Where `path.curved` is set to `true`.
* @description Creates a Two.js path that is curved and adds it to the scene.
* @nota-bene In either case of passing an array or passing numbered arguments the last argument is an optional `Boolean` that defines whether the path should be open or closed.
*/
makeCurve(...args: number[]): Path;
/**
* @name Two#makePolygon
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} radius
* @param {Number} sides
* @returns {Polygon}
* @description Creates a Two.js polygon and adds it to the scene.
*/
makePolygon(x: number, y: number, radius: number, sides: number): Polygon;
/**
* @name Two#makeArcSegment
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} innerRadius
* @param {Number} outerRadius
* @param {Number} startAngle
* @param {Number} endAngle
* @param {Number} [resolution=Two.Resolution] - The number of vertices that should comprise the arc segment.
* @returns {ArcSegment}
*/
makeArcSegment(
x: number,
y: number,
innerRadius: number,
outerRadius: number,
startAngle: number,
endAngle: number,
resolution?: number
): ArcSegment;
/**
* @name Two#makePoints
* @function
* @param {Vector[]} [points] - An array of {@link Two.Vector} points
* @returns {Points}
* @description Creates a Two.js points object and adds it to the current scene.
*/
makePoints(points?: Vector[]): Points;
/**
* @name Two#makePoints
* @function
* @param {...Number} [args] - Alternatively you can pass alternating `x` / `y` coordinate values as individual agrguments. These will be combined into {@link Two.Vector}s for use in the points object.
* @returns {Points}
* @description Creates a Two.js points object and adds it to the current scene.
*/
makePoints(...args: number[]): Points;
/**
* @name Two#makePath
* @function
* @param {Anchor[]} [points] - An array of {@link Two.Anchor} points
* @returns {Path}
* @description Creates a Two.js path and adds it to the scene.
* @nota-bene In either case of passing an array or passing numbered arguments the last argument is an optional `Boolean` that defines whether the path should be open or closed.
*/
makePath(points?: Anchor[]): Path;
/**
* @name Two#makePath
* @function
* @param {...Number} [args] - Alternatively you can pass alternating `x` / `y` coordinate values as individual arguments. These will be combined into {@link Two.Anchor}s for use in the path.
* @returns {Path}
* @description Creates a Two.js path and adds it to the scene.
* @nota-bene In either case of passing an array or passing numbered arguments the last argument is an optional `Boolean` that defines whether the path should be open or closed.
*/
makePath(...args: number[]): Path;
/**
* @name Two#makeText
* @function
* @param {String} message
* @param {Number} x
* @param {Number} y
* @param {Object} [styles] - An object to describe any of the {@link Two.Text.Properties} including `fill`, `stroke`, `linewidth`, `family`, `alignment`, `leading`, `opacity`, etc..
* @returns {Text}
* @description Creates a Two.js text object and adds it to the scene.
*/
makeText(
message: string,
x: number,
y: number,
styles?: {
value?: string;
family?: string;
size?: number;
leading?: number;
alignment?: 'left' | 'center' | 'right';
linewidth?: number;
style?: 'normal' | 'italic';
weight?: number | string;
decoration?: 'underline' | 'strikethrough' | 'none';
direction?: 'ltr' | 'rtl';
baseline?: 'top' | 'middle' | 'bottom' | 'baseline';
opacity?: number;
visible?: boolean;
fill?: string | Gradient | Texture;
stroke?: string | Gradient | Texture;
dashes?: number[] & { offset?: number };
}
): Text;
/**
* @name Two#makeLinearGradient
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* @param {...Stop} args - Any number of color stops sometimes referred to as ramp stops. If none are supplied then the default black-to-white two stop gradient is applied.
* @returns {LinearGradient}
* @description Creates a Two.js linear gradient and adds it to the scene. In the case of an effect it's added to an invisible "definitions" group.
*/
makeLinearGradient(
x1: number,
y1: number,
x2: number,
y2: number,
...args: Stop[]
): LinearGradient;
/**
* @name Two#makeRadialGradient
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} radius
* @param {...Stop} args - Any number of color stops sometimes referred to as ramp stops. If none are supplied then the default black-to-white two stop gradient is applied.
* @returns {RadialGradient}
* @description Creates a Two.js linear-gradient object and adds it to the scene. In the case of an effect it's added to an invisible "definitions" group.
*/
makeRadialGradient(
x1: number,
y1: number,
radius: number,
...args: Stop[]
): RadialGradient;
/**
* @name Two#makeSprite
* @function
* @param {(String|Texture)} src - The URL path to an image or an already created {@link Two.Texture}.
* @param {Number} x
* @param {Number} y
* @param {Number} [columns=1]
* @param {Number} [rows=1]
* @param {Number} [frameRate=0]
* @param {Boolean} [autostart=false]
* @returns {Sprite}
* @description Creates a Two.js sprite object and adds it to the scene. Sprites can be used for still images as well as animations.
*/
makeSprite(
src: any,
x: number,
y: number,
columns?: number,
rows?: number,
frameRate?: number,
autostart?: boolean
): Sprite;
/**
* @name Two#makeImage
* @function
* @param {(String|Two.Texture)} src - The URL path to an image or an already created {@link Two.Texture}.
* @param {Number} x
* @param {Number} y
* @param {Number} [width]
* @param {Number} [height]
* @param {String} [mode="fill"]
* @returns {Two.Image}
* @description Creates a Two.js image object and adds it to the scene. Images are scaled to fit the provided width and height.
*/
makeImage(
src: any,
x: number,
y: number,
width?: number,
height?: number,
mode?: 'fit' | 'fill' | 'crop' | 'tile' | 'stretch'
): Image;
/**
* @name Two#makeImageSequence
* @function
* @param {(String[]|Texture[])} src - An array of paths or of {@link Two.Textures}.
* @param {Number} x
* @param {Number} y
* @param {Number} [frameRate=0]
* @param {Boolean} [autostart=false]
* @returns {ImageSequence}
* @description Creates a Two.js image sequence object and adds it to the scene.
*/
makeImageSequence(
src: string[] | Texture[] | string | Texture,
x: number,
y: number,
frameRate?: number,
autostart?: boolean
): ImageSequence;
/**
* @name Two#makeTexture
* @function
* @param {(String|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement)} [src] - The URL path to an image or a DOM image-like element.
* @param {Function} [callback] - Function to be invoked when the image is loaded.
* @returns {Texture}
* @description Creates a Two.js texture object.
*/
makeTexture(src: any, callback?: () => void): Texture;
/**
* @name Two#makeGroup
* @function
* @param {Shape[]} [objects] - Two.js objects to be added to the group in the form of an array or as individual arguments.
* @returns {Group}
* @description Creates a Two.js group object and adds it to the scene.
*/
makeGroup(objects?: Shape[]): Group;
/**
* @name Two#makeGroup
* @function
* @param {...Shape} [args] - Alternatively pass each element as an argument
* @returns {Group}
* @description Creates a Two.js group object and adds it to the scene.
*/
makeGroup(...args: Shape[]): Group;
/**
* @name Two#interpret
* @function
* @param {SVGElement} svg - The SVG node to be parsed.
* @param {Boolean} shallow - Don't create a top-most group but append all content directly.
* @param {Boolean} [add=true] � Automatically add the reconstructed SVG node to scene.
* @returns {Group}
* @description Interpret an SVG Node and add it to this instance's scene. The distinction should be made that this doesn't `import` svg's, it solely interprets them into something compatible for Two.js - this is slightly different than a direct transcription.
*/
interpret(svg: SVGElement, shallow?: boolean, add?: boolean): Group;
/**
* @name Two#load
* @function
* @param {String|SVGElement} pathOrSVGContent - The URL path of an SVG file or an SVG document as text.
* @param {Function} [callback] - Function to call once loading has completed.
* @returns {Group}
* @description Load an SVG file or SVG text and interpret it into Two.js legible objects.
*/
load(
pathOrSVGContent: any,
callback?: (
group: Group,
svg:
| SVGElement
| SVGGElement
| SVGPathElement
| SVGTextElement
| SVGPatternElement
| SVGDefsElement
| SVGGradientElement
| SVGLinearGradientElement
| SVGRadialGradientElement
| SVGImageElement
| SVGClipPathElement
| SVGStopElement
| (
| SVGElement
| SVGGElement
| SVGPathElement
| SVGTextElement
| SVGPatternElement
| SVGDefsElement
| SVGGradientElement
| SVGLinearGradientElement
| SVGRadialGradientElement
| SVGImageElement
| SVGClipPathElement
| SVGStopElement
)[]
) => void
): Group;
}
import { Line } from 'two.js/src/shapes/line';
import { Path } from 'two.js/src/path';
import { Rectangle } from 'two.js/src/shapes/rectangle';
import { Circle } from 'two.js/src/shapes/circle';
import { Ellipse } from 'two.js/src/shapes/ellipse';
import { Star } from 'two.js/src/shapes/star';
import { Polygon } from 'two.js/src/shapes/polygon';
import { ArcSegment } from 'two.js/src/shapes/arc-segment';
import { Points } from 'two.js/src/shapes/points';
import { Text } from 'two.js/src/text';
import { LinearGradient } from 'two.js/src/effects/linear-gradient';
import { RadialGradient } from 'two.js/src/effects/radial-gradient';
import { Sprite } from 'two.js/src/effects/sprite';
import { Image } from 'two.js/src/effects/image';
import { ImageSequence } from 'two.js/src/effects/image-sequence';
import { Texture } from 'two.js/src/effects/texture';
import { Group } from 'two.js/src/group';
import { Anchor } from 'two.js/src/anchor';
import { Collection } from 'two.js/src/collection';
import { Events } from 'two.js/src/events';
import { Matrix } from 'two.js/src/matrix';
import { Registry } from 'two.js/src/registry';
import { Element as TwoElement } from 'two.js/src/element';
import { Shape, type ShapeHitTestOptions } from 'two.js/src/shape';
import { Vector } from 'two.js/src/vector';
import { Gradient } from 'two.js/src/effects/gradient';
import { Stop } from 'two.js/src/effects/stop';
import { RoundedRectangle } from 'two.js/src/shapes/rounded-rectangle';
import { Renderer as CanvasRenderer } from 'two.js/src/renderers/canvas';
import { Renderer as SVGRenderer } from 'two.js/src/renderers/svg';
import { Renderer as WebGLRenderer } from 'two.js/src/renderers/webgl';
export type BoundingBox = {
top: number;
left: number;
right: number;
bottom: number;
} & Dimensions;
export type Dimensions = {
width: number;
height: number;
};
export interface SceneHitTestOptions extends ShapeHitTestOptions {
visibleOnly?: boolean;
includeGroups?: boolean;
mode?: 'all' | 'deepest';
deepest?: boolean;
filter?: (shape: Shape) => boolean;
}
}
================================================
FILE: src/two.js
================================================
// Utils
import { CanvasPolyfill } from './utils/canvas-polyfill.js';
import * as Curves from './utils/curves.js';
import { dom } from './utils/dom.js';
import { TwoError } from './utils/error.js';
import { getRatio } from './utils/device-pixel-ratio.js';
import { read } from './utils/interpret-svg.js';
import * as math from './utils/math.js';
import { Commands } from './utils/path-commands.js';
import { _ } from './utils/underscore.js';
import { xhr } from './utils/xhr.js';
// Core Classes
import { Anchor } from './anchor.js';
import { Collection } from './collection.js';
import { Events } from './events.js';
import { Group } from './group.js';
import { Matrix } from './matrix.js';
import { Path } from './path.js';
import { Registry } from './registry.js';
import { Element } from './element.js';
import { Shape } from './shape.js';
import { Text } from './text.js';
import { Vector } from './vector.js';
// Effects
import { Stop } from './effects/stop.js';
import { Gradient } from './effects/gradient.js';
import { Image } from './effects/image.js';
import { ImageSequence } from './effects/image-sequence.js';
import { LinearGradient } from './effects/linear-gradient.js';
import { RadialGradient } from './effects/radial-gradient.js';
import { Sprite } from './effects/sprite.js';
import { Texture } from './effects/texture.js';
// Secondary Classes
import { ArcSegment } from './shapes/arc-segment.js';
import { Circle } from './shapes/circle.js';
import { Ellipse } from './shapes/ellipse.js';
import { Line } from './shapes/line.js';
import { Points } from './shapes/points.js';
import { Polygon } from './shapes/polygon.js';
import { Rectangle } from './shapes/rectangle.js';
import { RoundedRectangle } from './shapes/rounded-rectangle.js';
import { Star } from './shapes/star.js';
// Renderers
import { Renderer as CanvasRenderer } from './renderers/canvas.js';
import { Renderer as SVGRenderer } from './renderers/svg.js';
import { Renderer as WebGLRenderer } from './renderers/webgl.js';
import { Constants } from './constants.js';
const Utils = _.extend(
{
Error: TwoError,
getRatio,
read,
xhr,
},
_,
CanvasPolyfill,
Curves,
math
);
/**
* @name Two
* @class
* @global
* @extends Two.Events
* @param {Object} [options]
* @param {Boolean} [options.fullscreen=false] - Set to `true` to automatically make the stage adapt to the width and height of the parent document. This parameter overrides `width` and `height` parameters if set to `true`. This overrides `options.fitted` as well.
* @param {Boolean} [options.fitted=false] - Set to `true` to automatically make the stage adapt to the width and height of the parent element. This parameter overrides `width` and `height` parameters if set to `true`.
* @param {Number} [options.width=640] - The width of the stage on construction. This can be set at a later time.
* @param {Number} [options.height=480] - The height of the stage on construction. This can be set at a later time.
* @param {String} [options.type=Two.Types.svg] - The type of renderer to setup drawing with. See {@link Two.Types} for available options.
* @param {Boolean} [options.autostart=false] - Set to `true` to add the instance to draw on `requestAnimationFrame`. This is a convenient substitute for {@link Two#play}.
* @param {Element} [options.domElement] - The canvas or SVG element to draw into. This overrides the `options.type` argument.
* @description The entrypoint for Two.js. Instantiate a `new Two` in order to setup a scene to render to. `Two` is also the publicly accessible namespace that all other sub-classes, functions, and utilities attach to.
*/
export default class Two {
// Warning: inherit events while overriding static properties
/**
* @private
*/
_events = new Events();
// Getters and setters aren't enumerable
get _bound() {
return this._events._bound;
}
set _bound(v) {
this._events._bound = v;
}
addEventListener() {
return this._events.addEventListener?.apply(this, arguments);
}
on() {
return this._events.addEventListener?.apply(this, arguments);
}
bind() {
return this._events.addEventListener?.apply(this, arguments);
}
removeEventListener() {
return this._events.removeEventListener?.apply(this, arguments);
}
off() {
return this._events.removeEventListener?.apply(this, arguments);
}
unbind() {
return this._events.removeEventListener?.apply(this, arguments);
}
dispatchEvent() {
return this._events.dispatchEvent?.apply(this, arguments);
}
trigger() {
return this._events.dispatchEvent?.apply(this, arguments);
}
listen() {
return this._events.listen?.apply(this, arguments);
}
ignore() {
return this._events.ignore?.apply(this, arguments);
}
/**
* @name Two#type
* @property {String} - A string representing which type of renderer the instance has instantiated.
*/
type = '';
/**
* @name Two#renderer
* @property {(Two.SVGRenderer|Two.CanvasRenderer|Two.WebGLRenderer)} - The instantiated rendering class for the instance. For a list of possible rendering types check out Two.Types.
*/
renderer = null;
/**
* @name Two#scene
* @property {Two.Group} - The base level {@link Two.Group} which houses all objects for the instance. Because it is a {@link Two.Group} transformations can be applied to it that will affect all objects in the instance. This is handy as a makeshift inverted camera.
*/
scene = null;
/**
* @name Two#width
* @property {Number} - The width of the instance's dom element.
*/
width = 0;
/**
* @name Two#height
* @property {Number} - The height of the instance's dom element.
*/
height = 0;
/**
* @name Two#frameCount
* @property {Number} - An integer representing how many frames have elapsed.
*/
frameCount = 0;
/**
* @name Two#timeDelta
* @property {Number} - A number representing how much time has elapsed since the last frame in milliseconds.
*/
timeDelta = 0;
/**
* @name Two#playing
* @property {Boolean} - A boolean representing whether or not the instance is being updated through the automatic `requestAnimationFrame`.
*/
playing = false;
constructor(options) {
// Determine what Renderer to use and setup a scene.
const params = _.defaults(options || {}, {
fullscreen: false,
fitted: false,
width: 640,
height: 480,
type: Two.Types.svg,
autostart: false,
});
_.each(
params,
function (v, k) {
if (/fullscreen/i.test(k) || /autostart/i.test(k)) {
return;
}
this[k] = v;
},
this
);
// Specified domElement overrides type declaration only if the element does not support declared renderer type.
if (_.isElement(params.domElement)) {
const tagName = params.domElement.tagName.toLowerCase();
// TODO: Reconsider this if statement's logic.
if (
!/^(CanvasRenderer-canvas|WebGLRenderer-canvas|SVGRenderer-svg)$/.test(
this.type + '-' + tagName
)
) {
this.type = Two.Types[tagName];
}
}
this.renderer = new Two[this.type](this);
this.setPlaying(params.autostart);
this.frameCount = 0;
/**
* @name Two#fit
* @function
* @description If `options.fullscreen` or `options.fitted` in construction create this function. It sets the `width` and `height` of the instance to its respective parent `window` or `element` depending on the `options` passed.
*/
if (params.fullscreen) {
this.fit = fitToWindow.bind(this);
this.fit.domElement = window;
this.fit.attached = true;
_.extend(document.body.style, {
overflow: 'hidden',
margin: 0,
padding: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
position: 'fixed',
});
_.extend(this.renderer.domElement.style, {
display: 'block',
top: 0,
left: 0,
right: 0,
bottom: 0,
position: 'fixed',
});
dom.bind(this.fit.domElement, 'resize', this.fit);
this.fit();
} else if (params.fitted) {
this.fit = fitToParent.bind(this);
_.extend(this.renderer.domElement.style, {
display: 'block',
});
} else if (
typeof params.width === 'number' &&
typeof params.height === 'number'
) {
this.renderer.setSize(params.width, params.height, this.ratio);
this.width = params.width;
this.height = params.height;
}
this.renderer.bind(Events.Types.resize, updateDimensions.bind(this));
this.scene = this.renderer.scene;
Two.Instances.push(this);
if (params.autostart) {
raf.init();
}
}
static NextFrameId = Constants.NextFrameId;
// Primitive
/**
* @name Two.Types
* @property {Object} - The different rendering types available in the library.
*/
static Types = Constants.Types;
/**
* @name Two.Version
* @property {String} - The current working version of the library, `$version`.
*/
static Version = Constants.Version;
/**
* @name Two.PublishDate
* @property {String} - The automatically generated publish date in the build process to verify version release candidates.
*/
static PublishDate = Constants.PublishDate;
/**
* @name Two.Identifier
* @property {String} - String prefix for all Two.js object's ids. This trickles down to SVG ids.
*/
static Identifier = Constants.Identifier;
/**
* @name Two.Resolution
* @property {Number} - Default amount of vertices to be used for interpreting Arcs and ArcSegments.
*/
static Resolution = Constants.Resolution;
/**
* @name Two.AutoCalculateImportedMatrices
* @property {Boolean} - When importing SVGs through the {@link Two#interpret} and {@link Two#load}, this boolean determines whether Two.js infers and then overrides the exact transformation matrix of the reference SVG.
* @nota-bene `false` copies the exact transformation matrix values, but also sets the path's `matrix.manual = true`.
*/
static AutoCalculateImportedMatrices =
Constants.AutoCalculateImportedMatrices;
/**
* @name Two.Instances
* @property {Two[]} - Registered list of all Two.js instances in the current session.
*/
static Instances = Constants.Instances;
/**
* @function Two.uniqueId
* @description Simple method to access an incrementing value. Used for `id` allocation on all Two.js objects.
* @returns {Number} Ever increasing Number.
*/
static uniqueId = Constants.uniqueId;
static Anchor = Anchor;
static Collection = Collection;
static Events = Events;
static Group = Group;
static Matrix = Matrix;
static Path = Path;
static Registry = Registry;
static Element = Element;
static Shape = Shape;
static Text = Text;
static Vector = Vector;
static Gradient = Gradient;
static Image = Image;
static ImageSequence = ImageSequence;
static LinearGradient = LinearGradient;
static RadialGradient = RadialGradient;
static Sprite = Sprite;
static Stop = Stop;
static Texture = Texture;
static ArcSegment = ArcSegment;
static Circle = Circle;
static Ellipse = Ellipse;
static Line = Line;
static Points = Points;
static Polygon = Polygon;
static Rectangle = Rectangle;
static RoundedRectangle = RoundedRectangle;
static Star = Star;
static CanvasRenderer = CanvasRenderer;
static SVGRenderer = SVGRenderer;
static WebGLRenderer = WebGLRenderer;
/**
* @name Two.Commands
* @property {Object} - Map of possible path commands. Taken from the SVG specification. Commands include: `move`, `line`, `curve`, `arc`, and `close`.
*/
static Commands = Commands;
/**
* @name Two.Utils
* @property {Object} Utils - A massive object filled with utility functions and properties.
* @property {Object} Two.Utils.read - A collection of SVG parsing functions indexed by element name.
* @property {Function} Two.Utils.read.path - Parse SVG path element or `d` attribute string.
*/
static Utils = Utils;
/**
* @name Two#appendTo
* @function
* @param {Element} elem - The DOM element to append the Two.js stage to.
* @description Shorthand method to append your instance of Two.js to the `document`.
*/
appendTo(elem) {
elem.appendChild(this.renderer.domElement);
if (this.fit) {
if (this.fit.domElement !== window) {
this.fit.domElement = elem;
this.fit.attached = false;
}
this.update();
}
return this;
}
/**
* @name Two#play
* @function
* @fires play
* @description Call to start an internal animation loop.
* @nota-bene This function initiates a `requestAnimationFrame` loop.
*/
play() {
this.playing = true;
raf.init();
return this.trigger(Events.Types.play);
}
/**
* @name Two#pause
* @function
* @fires pause
* @description Call to stop the internal animation loop for a specific instance of Two.js.
*/
pause() {
this.playing = false;
return this.trigger(Events.Types.pause);
}
setPlaying(p) {
this.playing = p;
}
/**
* @name Two#release
* @function
* @param {Two.Element} [obj] - Object to release from event listening. If none provided then the root {@link Two.Group} will be used.
* @returns {Two.Element} The object passed for event deallocation.
* @description Release a {@link Two.Element}’s events from memory and recurse through its children, effects, and/or vertices.
*/
release(obj) {
let i, v, child;
// Release this instance of Two.js if nothing passed
if (typeof obj === 'undefined') {
return this.release(this.scene);
}
if (typeof obj.unbind === 'function') {
obj.unbind();
}
// Unbind effects applied to an object
if (typeof obj.fill === 'object' && typeof obj.fill.unbind === 'function') {
obj.fill.unbind();
}
if (
typeof obj.stroke === 'object' &&
typeof obj.stroke.unbind === 'function'
) {
obj.stroke.unbind();
}
// Unbind vertices on an object
if (obj.vertices) {
if (typeof obj.vertices.unbind === 'function') {
try {
obj.vertices.unbind();
} catch (e) {
// Ignore unbind errors for incomplete Collection objects
}
}
for (i = 0; i < obj.vertices.length; i++) {
v = obj.vertices[i];
if (typeof v.unbind === 'function') {
v.unbind();
}
if (v.controls) {
if (v.controls.left && typeof v.controls.left.unbind === 'function') {
v.controls.left.unbind();
}
if (
v.controls.right &&
typeof v.controls.right.unbind === 'function'
) {
v.controls.right.unbind();
}
}
}
}
// Unbind any children of the object
if (obj.children) {
for (i = 0; i < obj.children.length; i++) {
child = obj.children[i];
this.release(child);
}
if (typeof obj.children.unbind === 'function') {
try {
obj.children.unbind();
} catch (e) {
// Ignore unbind errors for incomplete Collection objects
}
}
}
// Clean up renderer-specific resources
if (obj._renderer) {
// SVG DOM element cleanup
if (obj._renderer.elem && obj._renderer.elem.parentNode) {
obj._renderer.elem.parentNode.removeChild(obj._renderer.elem);
delete obj._renderer.elem;
}
// WebGL resource cleanup
if (this.type === 'WebGLRenderer' && this.renderer.ctx) {
const gl = this.renderer.ctx;
// Clean up textures
if (obj._renderer.texture) {
gl.deleteTexture(obj._renderer.texture);
delete obj._renderer.texture;
}
// Clean up buffers
if (obj._renderer.positionBuffer) {
gl.deleteBuffer(obj._renderer.positionBuffer);
delete obj._renderer.positionBuffer;
}
// Clean up any other WebGL effects
if (obj._renderer.effect) {
obj._renderer.effect = null;
}
}
// Canvas renderer cleanup - clear cached contexts and data
if (this.type === 'CanvasRenderer' && obj._renderer.context) {
delete obj._renderer.context;
}
}
return obj;
}
/**
* @name Two#getShapesAtPoint
* @function
* @param {Number} x - X coordinate in world space.
* @param {Number} y - Y coordinate in world space.
* @param {Object} [options] - Hit test configuration.
* @param {Boolean} [options.visibleOnly=true] - Limit results to visible shapes.
* @param {Boolean} [options.includeGroups=false] - Include groups in the hit results.
* @param {('all'|'deepest')} [options.mode='all'] - Whether to return all intersecting shapes or only the top-most.
* @param {Boolean} [options.deepest] - Alias for `mode: 'deepest'`.
* @param {Number} [options.precision] - Segmentation precision for curved geometry.
* @param {Number} [options.tolerance=0] - Pixel tolerance applied to hit testing.
* @param {Boolean} [options.fill] - Override fill testing behaviour.
* @param {Boolean} [options.stroke] - Override stroke testing behaviour.
* @param {Function} [options.filter] - Predicate to filter shapes from the result set.
* @returns {Two.Shape[]} Ordered list of shapes under the specified point, front to back.
* @description Returns shapes underneath the provided coordinates. Coordinates are expected in world space (matching the renderer output).
* @nota-bene Delegates to {@link Two.Group#getShapesAtPoint} on the root scene.
*/
getShapesAtPoint(x, y, options) {
if (this.scene && typeof this.scene.getShapesAtPoint === 'function') {
return this.scene.getShapesAtPoint(x, y, options);
}
return [];
}
/**
* @name Two#update
* @function
* @fires update
* @description Update positions and calculations in one pass before rendering. Then render to the canvas.
* @nota-bene This function is called automatically if using {@link Two#play} or the `autostart` parameter in construction.
*/
update() {
const animated = !!this._lastFrame;
const now = _.performance.now();
if (animated) {
this.timeDelta = parseFloat((now - this._lastFrame).toFixed(3));
}
this._lastFrame = now;
if (this.fit && this.fit.domElement && !this.fit.attached) {
dom.bind(this.fit.domElement, 'resize', this.fit);
this.fit.attached = true;
this.fit();
}
const width = this.width;
const height = this.height;
const renderer = this.renderer;
// Update width / height for the renderer
if (width !== renderer.width || height !== renderer.height) {
renderer.setSize(width, height, this.ratio);
}
this.trigger(Events.Types.update, this.frameCount, this.timeDelta);
return this.render();
}
/**
* @name Two#render
* @function
* @fires render
* @description Render all drawable and visible objects of the scene.
*/
render() {
this.renderer.render();
return this.trigger(Events.Types.render, this.frameCount++);
}
// Convenience Methods
/**
* @name Two#add
* @function
* @param {(Two.Shape[]|...Two.Shape)} [objects] - An array of Two.js objects. Alternatively can add objects as individual arguments.
* @description A shorthand method to add specific Two.js objects to the scene.
*/
add(objects) {
if (!(objects instanceof Array)) {
objects = Array.prototype.slice.call(arguments);
}
this.scene.add(objects);
return this;
}
/**
* @name Two#remove
* @function
* @param {(Two.Shape[]|...Two.Shape)} [objects] - An array of Two.js objects.
* @description A shorthand method to remove specific Two.js objects from the scene.
*/
remove(objects) {
if (!(objects instanceof Array)) {
objects = Array.prototype.slice.call(arguments);
}
this.scene.remove(objects);
return this;
}
/**
* @name Two#clear
* @function
* @description Removes all objects from the instance's scene. If you intend to have the browser garbage collect this, don't forget to delete the references in your application as well.
*/
clear() {
this.scene.remove(this.scene.children);
return this;
}
/**
* @name Two#makeLine
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* @returns {Two.Line}
* @description Creates a Two.js line and adds it to the scene.
*/
makeLine(x1, y1, x2, y2) {
const line = new Line(x1, y1, x2, y2);
this.scene.add(line);
return line;
}
/**
* @name Two#makeArrow
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* @returns {Two.Path}
* @description Creates a Two.js arrow and adds it to the scene.
*/
makeArrow(x1, y1, x2, y2, size) {
const headlen = typeof size === 'number' ? size : 10;
const angle = Math.atan2(y2 - y1, x2 - x1);
const vertices = [
new Anchor(
x1,
y1,
undefined,
undefined,
undefined,
undefined,
Commands.move
),
new Anchor(
x2,
y2,
undefined,
undefined,
undefined,
undefined,
Commands.line
),
new Anchor(
x2 - headlen * Math.cos(angle - Math.PI / 4),
y2 - headlen * Math.sin(angle - Math.PI / 4),
undefined,
undefined,
undefined,
undefined,
Commands.line
),
new Anchor(
x2,
y2,
undefined,
undefined,
undefined,
undefined,
Commands.move
),
new Anchor(
x2 - headlen * Math.cos(angle + Math.PI / 4),
y2 - headlen * Math.sin(angle + Math.PI / 4),
undefined,
undefined,
undefined,
undefined,
Commands.line
),
];
const path = new Path(vertices, false, false, true);
path.noFill();
path.cap = 'round';
path.join = 'round';
this.scene.add(path);
return path;
}
/**
* @name Two#makeRectangle
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} width
* @param {Number} height
* @returns {Two.Rectangle}
* @description Creates a Two.js rectangle and adds it to the scene.
*/
makeRectangle(x, y, width, height) {
const rect = new Rectangle(x, y, width, height);
this.scene.add(rect);
return rect;
}
/**
* @name Two#makeRoundedRectangle
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} width
* @param {Number} height
* @param {Number} sides
* @returns {Two.RoundedRectangle}
* @description Creates a Two.js rounded rectangle and adds it to the scene.
*/
makeRoundedRectangle(x, y, width, height, sides) {
const rect = new RoundedRectangle(x, y, width, height, sides);
this.scene.add(rect);
return rect;
}
/**
* @name Two#makeCircle
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} radius
* @param {Number} [resolution=4]
* @returns {Two.Circle}
* @description Creates a Two.js circle and adds it to the scene.
*/
makeCircle(x, y, radius, resolution) {
const circle = new Circle(x, y, radius, resolution);
this.scene.add(circle);
return circle;
}
/**
* @name Two#makeEllipse
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} rx
* @param {Number} ry
* @param {Number} [resolution=4]
* @returns {Two.Ellipse}
* @description Creates a Two.js ellipse and adds it to the scene.
*/
makeEllipse(x, y, rx, ry, resolution) {
const ellipse = new Ellipse(x, y, rx, ry, resolution);
this.scene.add(ellipse);
return ellipse;
}
/**
* @name Two#makeStar
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} outerRadius
* @param {Number} innerRadius
* @param {Number} sides
* @returns {Two.Star}
* @description Creates a Two.js star and adds it to the scene.
*/
makeStar(x, y, outerRadius, innerRadius, sides) {
const star = new Star(x, y, outerRadius, innerRadius, sides);
this.scene.add(star);
return star;
}
/**
* @name Two#makeCurve
* @function
* @param {Two.Anchor[]} [points] - An array of {@link Two.Anchor} points.
* @param {...Number} - Alternatively you can pass alternating `x` / `y` coordinate values as individual arguments. These will be combined into {@link Two.Anchor}s for use in the path.
* @returns {Two.Path} - Where `path.curved` is set to `true`.
* @description Creates a Two.js path that is curved and adds it to the scene.
* @nota-bene In either case of passing an array or passing numbered arguments the last argument is an optional `Boolean` that defines whether the path should be open or closed.
*/
makeCurve(points) {
const l = arguments.length;
if (!Array.isArray(points)) {
points = [];
for (let i = 0; i < l; i += 2) {
const x = arguments[i];
if (typeof x !== 'number') {
break;
}
const y = arguments[i + 1];
points.push(new Anchor(x, y));
}
}
const last = arguments[l - 1];
const curve = new Path(
points,
!(typeof last === 'boolean' ? last : undefined),
true
);
const rect = curve.getBoundingClientRect();
curve
.center()
.translation.set(rect.left + rect.width / 2, rect.top + rect.height / 2);
this.scene.add(curve);
return curve;
}
/**
* @name Two#makePolygon
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} radius
* @param {Number} sides
* @returns {Two.Polygon}
* @description Creates a Two.js polygon and adds it to the scene.
*/
makePolygon(x, y, radius, sides) {
const poly = new Polygon(x, y, radius, sides);
this.scene.add(poly);
return poly;
}
/**
* @name Two#makeArcSegment
* @function
* @param {Number} x
* @param {Number} y
* @param {Number} innerRadius
* @param {Number} outerRadius
* @param {Number} startAngle
* @param {Number} endAngle
* @param {Number} [resolution=Two.Resolution] - The number of vertices that should comprise the arc segment.
* @returns {Two.ArcSegment}
*/
makeArcSegment(
x,
y,
innerRadius,
outerRadius,
startAngle,
endAngle,
resolution
) {
const arcSegment = new ArcSegment(
x,
y,
innerRadius,
outerRadius,
startAngle,
endAngle,
resolution
);
this.scene.add(arcSegment);
return arcSegment;
}
/**
* @name Two#makePoints
* @function
* @param {Two.Vector[]} [points] - An array of {@link Two.Vector} points
* @param {...Number} - Alternatively you can pass alternating `x` / `y` coordinate values as individual agrguments. These will be combined into {@link Two.Vector}s for use in the points object.
* @returns {Two.Points}
* @description Creates a Two.js points object and adds it to the current scene.
*/
makePoints(p) {
const l = arguments.length;
let vertices = p;
if (!Array.isArray(p)) {
vertices = [];
for (let i = 0; i < l; i += 2) {
const x = arguments[i];
if (typeof x !== 'number') {
break;
}
const y = arguments[i + 1];
vertices.push(new Vector(x, y));
}
}
const points = new Points(vertices);
this.scene.add(points);
return points;
}
/**
* @name Two#makePath
* @function
* @param {Two.Anchor[]} [points] - An array of {@link Two.Anchor} points
* @param {...Number} - Alternatively you can pass alternating `x` / `y` coordinate values as individual arguments. These will be combined into {@link Two.Anchor}s for use in the path.
* @returns {Two.Path}
* @description Creates a Two.js path and adds it to the scene.
* @nota-bene In either case of passing an array or passing numbered arguments the last argument is an optional `Boolean` that defines whether the path should be open or closed.
*/
makePath(p) {
const l = arguments.length;
let points = p;
if (!Array.isArray(p)) {
points = [];
for (let i = 0; i < l; i += 2) {
const x = arguments[i];
if (typeof x !== 'number') {
break;
}
const y = arguments[i + 1];
points.push(new Anchor(x, y));
}
}
const last = arguments[l - 1];
const path = new Path(
points,
!(typeof last === 'boolean' ? last : undefined)
);
const rect = path.getBoundingClientRect();
if (
typeof rect.top === 'number' &&
typeof rect.left === 'number' &&
typeof rect.right === 'number' &&
typeof rect.bottom === 'number'
) {
path
.center()
.translation.set(
rect.left + rect.width / 2,
rect.top + rect.height / 2
);
}
this.scene.add(path);
return path;
}
/**
* @name Two#makeText
* @function
* @param {String} message
* @param {Number} x
* @param {Number} y
* @param {Object} [styles] - An object to describe any of the {@link Two.Text.Properties} including `fill`, `stroke`, `linewidth`, `family`, `alignment`, `leading`, `opacity`, etc..
* @returns {Two.Text}
* @description Creates a Two.js text object and adds it to the scene.
*/
makeText(message, x, y, styles) {
const text = new Text(message, x, y, styles);
this.add(text);
return text;
}
/**
* @name Two#makeLinearGradient
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* @param {...Two.Stop} args - Any number of color stops sometimes referred to as ramp stops. If none are supplied then the default black-to-white two stop gradient is applied.
* @returns {Two.LinearGradient}
* @description Creates a Two.js linear gradient and adds it to the scene. In the case of an effect it's added to an invisible "definitions" group.
*/
makeLinearGradient(x1, y1, x2, y2 /* stops */) {
const stops = Array.prototype.slice.call(arguments, 4);
const gradient = new LinearGradient(x1, y1, x2, y2, stops);
this.add(gradient);
return gradient;
}
/**
* @name Two#makeRadialGradient
* @function
* @param {Number} x1
* @param {Number} y1
* @param {Number} radius
* @param {...Two.Stop} args - Any number of color stops sometimes referred to as ramp stops. If none are supplied then the default black-to-white two stop gradient is applied.
* @returns {Two.RadialGradient}
* @description Creates a Two.js linear-gradient object and adds it to the scene. In the case of an effect it's added to an invisible "definitions" group.
*/
makeRadialGradient(x1, y1, radius /* stops */) {
const stops = Array.prototype.slice.call(arguments, 3);
const gradient = new RadialGradient(x1, y1, radius, stops);
this.add(gradient);
return gradient;
}
/**
* @name Two#makeSprite
* @function
* @param {(String|Two.Texture)} src - The URL path to an image or an already created {@link Two.Texture}.
* @param {Number} x
* @param {Number} y
* @param {Number} [columns=1]
* @param {Number} [rows=1]
* @param {Number} [frameRate=0]
* @param {Boolean} [autostart=false]
* @returns {Two.Sprite}
* @description Creates a Two.js sprite object and adds it to the scene. Sprites can be used for still images as well as animations.
*/
makeSprite(src, x, y, columns, rows, frameRate, autostart) {
const sprite = new Sprite(src, x, y, columns, rows, frameRate);
if (autostart) {
sprite.play();
}
this.add(sprite);
return sprite;
}
/**
* @name Two#makeImage
* @function
* @param {(String|Two.Texture)} src - The URL path to an image or an already created {@link Two.Texture}.
* @param {Number} x
* @param {Number} y
* @param {Number} width
* @param {Number} height
* @param {String} [mode="fill"]
* @returns {Two.Image}
* @description Creates a Two.js image object and adds it to the scene. Images are scaled to fit the provided width and height.
*/
makeImage(src, x, y, width, height, mode) {
const image = new Image(src, x, y, width, height, mode);
this.add(image);
return image;
}
/**
* @name Two#makeImageSequence
* @function
* @param {(String[]|Two.Texture[])} src - An array of paths or of {@link Two.Textures}.
* @param {Number} x
* @param {Number} y
* @param {Number} [frameRate=0]
* @param {Boolean} [autostart=false]
* @returns {Two.ImageSequence}
* @description Creates a Two.js image sequence object and adds it to the scene.
*/
makeImageSequence(src, x, y, frameRate, autostart) {
const imageSequence = new ImageSequence(src, x, y, frameRate);
if (autostart) {
imageSequence.play();
}
this.add(imageSequence);
return imageSequence;
}
/**
* @name Two#makeTexture
* @function
* @param {(String|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement)} [src] - The URL path to an image or a DOM image-like element.
* @param {Function} [callback] - Function to be invoked when the image is loaded.
* @returns {Two.Texture}
* @description Creates a Two.js texture object.
*/
makeTexture(src, callback) {
const texture = new Texture(src, callback);
return texture;
}
/**
* @name Two#makeGroup
* @function
* @param {(Two.Shape[]|...Two.Shape)} [objects] - Two.js objects to be added to the group in the form of an array or as individual arguments.
* @returns {Two.Group}
* @description Creates a Two.js group object and adds it to the scene.
*/
makeGroup(objects) {
if (!(objects instanceof Array)) {
objects = Array.prototype.slice.call(arguments);
}
const group = new Group();
this.scene.add(group);
group.add(objects);
return group;
}
/**
* @name Two#interpret
* @function
* @param {SVGElement} svg - The SVG node to be parsed.
* @param {Boolean} shallow - Don't create a top-most group but append all content directly.
* @param {Boolean} [add=true] – Automatically add the reconstructed SVG node to scene.
* @returns {Two.Group}
* @description Interpret an SVG Node and add it to this instance's scene. The distinction should be made that this doesn't `import` svg's, it solely interprets them into something compatible for Two.js - this is slightly different than a direct transcription.
*/
interpret(svg, shallow, add) {
const tag = svg.tagName.toLowerCase();
add = typeof add !== 'undefined' ? add : true;
if (!(tag in read)) {
return null;
}
const node = read[tag].call(this, svg);
if (add) {
this.add(shallow && node instanceof Group ? node.children : node);
} else if (node.parent) {
// Remove `g` tags that have been added to scenegraph / DOM
// in order to be compatible with `getById` methods.
node.remove();
}
return node;
}
/**
* @name Two#load
* @function
* @param {String|SVGElement} pathOrSVGContent - The URL path of an SVG file or an SVG document as text.
* @param {Function} [callback] - Function to call once loading has completed.
* @returns {Two.Group}
* @description Load an SVG file or SVG text and interpret it into Two.js legible objects.
*/
load(pathOrSVGContent, callback) {
const group = new Group();
let elem, i, child;
const attach = function (data) {
dom.temp.innerHTML = data;
for (i = 0; i < dom.temp.children.length; i++) {
elem = dom.temp.children[i];
child = this.interpret(elem, false, false);
if (child !== null) {
group.add(child);
}
}
if (typeof callback === 'function') {
const svg =
dom.temp.children.length <= 1
? dom.temp.children[0]
: dom.temp.children;
callback(group, svg);
}
}.bind(this);
if (/\.svg$/i.test(pathOrSVGContent)) {
xhr(pathOrSVGContent, attach);
return group;
}
attach(pathOrSVGContent);
return group;
}
}
function fitToWindow() {
const wr = document.body.getBoundingClientRect();
const width = (this.width = wr.width);
const height = (this.height = wr.height);
this.renderer.setSize(width, height, this.ratio);
}
function fitToParent() {
const parent = this.renderer.domElement.parentElement;
if (!parent) {
console.warn('Two.js: Attempting to fit to parent, but no parent found.');
return;
}
const wr = parent.getBoundingClientRect();
const width = (this.width = wr.width);
const height = (this.height = wr.height);
this.renderer.setSize(width, height, this.ratio);
}
function updateDimensions(width, height) {
this.width = width;
this.height = height;
this.trigger(Events.Types.resize, width, height);
}
// Request Animation Frame
const raf = dom.getRequestAnimationFrame();
function loop() {
for (let i = 0; i < Two.Instances.length; i++) {
const t = Two.Instances[i];
if (t.playing) {
t.update();
}
}
Two.NextFrameId = raf(loop);
}
raf.init = function () {
loop();
raf.init = function () {};
};
================================================
FILE: src/utils/canvas-polyfill.d.ts
================================================
declare module 'two.js/src/utils/canvas-polyfill' {
export interface CanvasPolyfill {
Image: any;
isHeadless: boolean;
shim(canvas: any, name?: string): any;
polyfill(
canvas: any,
Image?: new (width?: number, height?: number) => HTMLImageElement
): any;
}
}
================================================
FILE: src/utils/canvas-polyfill.js
================================================
export const CanvasPolyfill = {
/**
* @param {Image}
*/
Image: null,
/**
* @param {Boolean}
*/
isHeadless: false,
/**
*
* @param {canvas} elem - An element to spoof as a ``.
* @param {String} [name] - An optional tag and node name to spoof. Defaults to `'canvas'`.
* @returns {canvas} - The same `elem` passed in the first argument with updated attributes needed to be used by Two.js.
* @description Adds attributes invoked by Two.js in order to execute and run correctly. This is used by headless environments.
*/
shim: function (elem, name) {
elem.tagName = elem.nodeName = name || 'canvas';
elem.nodeType = 1;
elem.getAttribute = function (prop) {
return this[prop];
};
elem.setAttribute = function (prop, val) {
this[prop] = val;
return this;
};
return elem;
},
/**
* @name Two.Utils.polyfill
* @function
* @param {canvas} canvas - The instanced `Canvas` object provided by `node-canvas`.
* @param {Image} [Image] - The prototypical `Image` object provided by `node-canvas`. This is only necessary to pass if you're going to load bitmap imagery.
* @returns {canvas} Returns the instanced canvas object you passed from with additional attributes needed for Two.js.
* @description Convenience method for defining all the dependencies from the npm package `node-canvas`. See [node-canvas](https://github.com/Automattic/node-canvas) for additional information on setting up HTML5 `` drawing in a node.js environment.
*/
polyfill: function (canvas, Image) {
CanvasPolyfill.shim(canvas);
if (typeof Image !== 'undefined') {
CanvasPolyfill.Image = Image;
}
CanvasPolyfill.isHeadless = true;
return canvas;
},
};
================================================
FILE: src/utils/curves.d.ts
================================================
declare module 'two.js/src/utils/curves' {
export interface Curve {
CollinearityEpsilon: number;
RecursionLimit: number;
CuspLimit: number;
Tolerance: {
distance: number;
angle: number;
epsilon: number;
};
abscissas: number[][];
weights: number[][];
}
/**
* @name Two.Utils.getComponentOnCubicBezier
* @function
* @param {Number} t - Zero-to-one value describing what percentage to calculate.
* @param {Number} a - The firt point's component value.
* @param {Number} b - The first point's bezier component value.
* @param {Number} c - The second point's bezier component value.
* @param {Number} d - The second point's component value.
* @returns {Number} The coordinate value for a specific component along a cubic bezier curve by `t`.
*/
export function getComponentOnCubicBezier(
t: number,
a: number,
b: number,
c: number,
d: number
): number;
/**
* @name Two.Utils.subdivide
* @function
* @param {Number} x1 - x position of first anchor point.
* @param {Number} y1 - y position of first anchor point.
* @param {Number} x2 - x position of first anchor point's "right" bezier handle.
* @param {Number} y2 - y position of first anchor point's "right" bezier handle.
* @param {Number} x3 - x position of second anchor point's "left" bezier handle.
* @param {Number} y3 - y position of second anchor point's "left" bezier handle.
* @param {Number} x4 - x position of second anchor point.
* @param {Number} y4 - y position of second anchor point.
* @param {Number} [limit=Two.Utils.Curve.RecursionLimit] - The amount of vertices to create by subdividing.
* @returns {Anchor[]} A list of anchor points ordered in between `x1`, `y1` and `x4`, `y4`
* @description Given 2 points (a, b) and corresponding control point for each return an array of points that represent points plotted along the curve. The number of returned points is determined by `limit`.
*/
export function subdivide(
x1: number,
y1: number,
x2: number,
y2: number,
x3: number,
y3: number,
x4: number,
y4: number,
limit?: number
): Anchor[];
/**
* @name Two.Utils.getCurveLength
* @function
* @param {Number} x1 - x position of first anchor point.
* @param {Number} y1 - y position of first anchor point.
* @param {Number} x2 - x position of first anchor point's "right" bezier handle.
* @param {Number} y2 - y position of first anchor point's "right" bezier handle.
* @param {Number} x3 - x position of second anchor point's "left" bezier handle.
* @param {Number} y3 - y position of second anchor point's "left" bezier handle.
* @param {Number} x4 - x position of second anchor point.
* @param {Number} y4 - y position of second anchor point.
* @param {Number} [limit=Two.Utils.Curve.RecursionLimit] - The amount of vertices to create by subdividing.
* @returns {Number} The length of a curve.
* @description Given 2 points (a, b) and corresponding control point for each, return a float that represents the length of the curve using Gauss-Legendre algorithm. Limit iterations of calculation by `limit`.
*/
export function getCurveLength(
x1: number,
y1: number,
x2: number,
y2: number,
x3: number,
y3: number,
x4: number,
y4: number,
limit?: number
): number;
/**
* @name Two.Utils.getCurveBoundingBox
* @function
* @param {Number} x1 - x position of first anchor point.
* @param {Number} y1 - y position of first anchor point.
* @param {Number} x2 - x position of first anchor point's "right" bezier handle.
* @param {Number} y2 - y position of first anchor point's "right" bezier handle.
* @param {Number} x3 - x position of second anchor point's "left" bezier handle.
* @param {Number} y3 - y position of second anchor point's "left" bezier handle.
* @param {Number} x4 - x position of second anchor point.
* @param {Number} y4 - y position of second anchor point.
* @returns {Object} Object contains min and max `x` / `y` bounds.
* @see {@link https://github.com/adobe-webplatform/Snap.svg/blob/master/src/path.js#L856}
*/
export function getCurveBoundingBox(
x1: number,
y1: number,
x2: number,
y2: number,
x3: number,
y3: number,
x4: number,
y4: number
): any;
/**
* @name Two.Utils.integrate
* @function
* @param {Function} f
* @param {Number} a
* @param {Number} b
* @param {Number} n
* @description Integration for `getCurveLength` calculations.
* @see [Paper.js](@link https://github.com/paperjs/paper.js/blob/master/src/util/Numerical.js#L101)
*/
export function integrate(
f: Function,
a: number,
b: number,
n: number
): number;
/**
* @name Two.Utils.getCurveFromPoints
* @function
* @param {Anchor[]} points
* @param {Boolean} closed
* @description Sets the bezier handles on {@link Anchor}s in the `points` list with estimated values to create a catmull-rom like curve. Used by {@link Two.Path#plot}.
*/
export function getCurveFromPoints(points: Anchor[], closed: boolean): void;
/**
* @name Two.Utils.getControlPoints
* @function
* @param {Anchor} a
* @param {Anchor} b
* @param {Anchor} c
* @returns {Anchor} Returns the passed middle point `b`.
* @description Given three coordinates set the control points for the middle, b, vertex based on its position with the adjacent points.
*/
export function getControlPoints(a: Anchor, b: Anchor, c: Anchor): Anchor;
/**
* @name Two.Utils.getReflection
* @function
* @param {Vector} a
* @param {Vector} b
* @param {Boolean} [relative=false]
* @returns {Vector} New {@link Vector} that represents the reflection point.
* @description Get the reflection of a point `b` about point `a`. Where `a` is in absolute space and `b` is relative to `a`.
* @see {@link http://www.w3.org/TR/SVG11/implnote.html#PathElementImplementationNotes}
*/
export function getReflection(
a: Vector,
b: Vector,
relative?: boolean
): Vector;
/**
* @name Two.Utils.getAnchorsFromArcData
* @function
* @param {Vector} center
* @param {Number} xAxisRotation
* @param {Number} rx - x radius
* @param {Number} ry - y radius
* @param {Number} ts
* @param {Number} td
* @param {Boolean} [ccw=false] - Set path traversal to counter-clockwise
*/
export function getAnchorsFromArcData(
center: Vector,
xAxisRotation: number,
rx: number,
ry: number,
ts: number,
td: number,
ccw?: boolean
): void;
import { Anchor } from 'two.js/src/anchor';
import { Vector } from 'two.js/src/vector';
}
================================================
FILE: src/utils/curves.js
================================================
import { mod, HALF_PI } from './math.js';
import { Commands } from './path-commands.js';
import { Anchor } from '../anchor.js';
import { Vector } from '../vector.js';
import { Constants } from '../constants.js';
/**
* @name Two.Utils.Curve
* @property {Object} - Additional utility constant variables related to curve math and calculations.
*/
const Curve = {
CollinearityEpsilon: Math.pow(10, -30),
RecursionLimit: 16,
CuspLimit: 0,
Tolerance: {
distance: 0.25,
angle: 0,
epsilon: Number.EPSILON,
},
// Lookup tables for abscissas and weights with values for n = 2 .. 16.
// As values are symmetric, only store half of them and adapt algorithm
// to factor in symmetry.
abscissas: [
[0.5773502691896],
[0, 0.7745966692415],
[0.3399810435849, 0.8611363115941],
[0, 0.5384693101057, 0.9061798459387],
[0.2386191860832, 0.6612093864663, 0.9324695142032],
[0, 0.4058451513774, 0.7415311855994, 0.9491079123428],
[0.1834346424956, 0.5255324099163, 0.7966664774136, 0.9602898564975],
[0, 0.3242534234038, 0.6133714327006, 0.8360311073266, 0.9681602395076],
[
0.1488743389816, 0.4333953941292, 0.679409568299, 0.865063366689,
0.9739065285172,
],
[
0, 0.2695431559523, 0.5190961292068, 0.730152005574, 0.8870625997681,
0.9782286581461,
],
[
0.1252334085115, 0.3678314989982, 0.5873179542866, 0.7699026741943,
0.9041172563705, 0.9815606342467,
],
[
0, 0.2304583159551, 0.4484927510364, 0.6423493394403, 0.8015780907333,
0.917598399223, 0.9841830547186,
],
[
0.1080549487073, 0.3191123689279, 0.5152486363582, 0.6872929048117,
0.8272013150698, 0.9284348836636, 0.9862838086968,
],
[
0, 0.2011940939974, 0.3941513470776, 0.5709721726085, 0.7244177313602,
0.8482065834104, 0.9372733924007, 0.9879925180205,
],
[
0.0950125098376, 0.2816035507793, 0.4580167776572, 0.6178762444026,
0.755404408355, 0.8656312023878, 0.9445750230732, 0.9894009349916,
],
],
weights: [
[1],
[0.8888888888889, 0.5555555555556],
[0.6521451548625, 0.3478548451375],
[0.5688888888889, 0.4786286704994, 0.2369268850562],
[0.4679139345727, 0.3607615730481, 0.1713244923792],
[0.4179591836735, 0.3818300505051, 0.2797053914893, 0.1294849661689],
[0.3626837833784, 0.3137066458779, 0.2223810344534, 0.1012285362904],
[
0.3302393550013, 0.31234707704, 0.2606106964029, 0.1806481606949,
0.0812743883616,
],
[
0.2955242247148, 0.26926671931, 0.219086362516, 0.1494513491506,
0.0666713443087,
],
[
0.2729250867779, 0.2628045445102, 0.233193764592, 0.1862902109277,
0.1255803694649, 0.0556685671162,
],
[
0.2491470458134, 0.2334925365384, 0.2031674267231, 0.1600783285433,
0.1069393259953, 0.0471753363865,
],
[
0.2325515532309, 0.2262831802629, 0.2078160475369, 0.1781459807619,
0.1388735102198, 0.0921214998377, 0.0404840047653,
],
[
0.2152638534632, 0.2051984637213, 0.1855383974779, 0.1572031671582,
0.1215185706879, 0.0801580871598, 0.0351194603318,
],
[
0.2025782419256, 0.1984314853271, 0.1861610000156, 0.166269205817,
0.1395706779262, 0.1071592204672, 0.0703660474881, 0.0307532419961,
],
[
0.1894506104551, 0.1826034150449, 0.169156519395, 0.1495959888166,
0.1246289712555, 0.0951585116825, 0.0622535239386, 0.0271524594118,
],
],
};
/**
* @name Two.Utils.getComponentOnCubicBezier
* @function
* @param {Number} t - Zero-to-one value describing what percentage to calculate.
* @param {Number} a - The firt point's component value.
* @param {Number} b - The first point's bezier component value.
* @param {Number} c - The second point's bezier component value.
* @param {Number} d - The second point's component value.
* @returns {Number} The coordinate value for a specific component along a cubic bezier curve by `t`.
*/
function getComponentOnCubicBezier(t, a, b, c, d) {
const k = 1 - t;
return k * k * k * a + 3 * k * k * t * b + 3 * k * t * t * c + t * t * t * d;
}
/**
* @name Two.Utils.subdivide
* @function
* @param {Number} x1 - x position of first anchor point.
* @param {Number} y1 - y position of first anchor point.
* @param {Number} x2 - x position of first anchor point's "right" bezier handle.
* @param {Number} y2 - y position of first anchor point's "right" bezier handle.
* @param {Number} x3 - x position of second anchor point's "left" bezier handle.
* @param {Number} y3 - y position of second anchor point's "left" bezier handle.
* @param {Number} x4 - x position of second anchor point.
* @param {Number} y4 - y position of second anchor point.
* @param {Number} [limit=Two.Utils.Curve.RecursionLimit] - The amount of vertices to create by subdividing.
* @returns {Anchor[]} A list of anchor points ordered in between `x1`, `y1` and `x4`, `y4`
* @description Given 2 points (a, b) and corresponding control point for each return an array of points that represent points plotted along the curve. The number of returned points is determined by `limit`.
*/
function subdivide(x1, y1, x2, y2, x3, y3, x4, y4, limit) {
limit = limit || Curve.RecursionLimit;
const amount = limit + 1;
// TODO: Abstract 0.001 to a limiting variable
// Don't recurse if the end points are identical
if (Math.abs(x1 - x4) < 0.001 && Math.abs(y1 - y4) < 0.001) {
return [new Anchor(x4, y4)];
}
const result = [];
for (let i = 0; i < amount; i++) {
const t = i / amount;
const x = getComponentOnCubicBezier(t, x1, x2, x3, x4);
const y = getComponentOnCubicBezier(t, y1, y2, y3, y4);
result.push(new Anchor(x, y));
}
return result;
}
/**
* @name Two.Utils.getCurveLength
* @function
* @param {Number} x1 - x position of first anchor point.
* @param {Number} y1 - y position of first anchor point.
* @param {Number} x2 - x position of first anchor point's "right" bezier handle.
* @param {Number} y2 - y position of first anchor point's "right" bezier handle.
* @param {Number} x3 - x position of second anchor point's "left" bezier handle.
* @param {Number} y3 - y position of second anchor point's "left" bezier handle.
* @param {Number} x4 - x position of second anchor point.
* @param {Number} y4 - y position of second anchor point.
* @param {Number} [limit=Two.Utils.Curve.RecursionLimit] - The amount of vertices to create by subdividing.
* @returns {Number} The length of a curve.
* @description Given 2 points (a, b) and corresponding control point for each, return a float that represents the length of the curve using Gauss-Legendre algorithm. Limit iterations of calculation by `limit`.
*/
function getCurveLength(x1, y1, x2, y2, x3, y3, x4, y4, limit) {
// TODO: Better / fuzzier equality check
// Linear calculation
if (x1 === x2 && y1 === y2 && x3 === x4 && y3 === y4) {
const dx = x4 - x1;
const dy = y4 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
// Calculate the coefficients of a Bezier derivative.
const ax = 9 * (x2 - x3) + 3 * (x4 - x1),
bx = 6 * (x1 + x3) - 12 * x2,
cx = 3 * (x2 - x1),
ay = 9 * (y2 - y3) + 3 * (y4 - y1),
by = 6 * (y1 + y3) - 12 * y2,
cy = 3 * (y2 - y1);
function integrand(t) {
// Calculate quadratic equations of derivatives for x and y
const dx = (ax * t + bx) * t + cx,
dy = (ay * t + by) * t + cy;
return Math.sqrt(dx * dx + dy * dy);
}
return integrate(integrand, 0, 1, limit || Curve.RecursionLimit);
}
/**
* @name Two.Utils.getCurveBoundingBox
* @function
* @param {Number} x1 - x position of first anchor point.
* @param {Number} y1 - y position of first anchor point.
* @param {Number} x2 - x position of first anchor point's "right" bezier handle.
* @param {Number} y2 - y position of first anchor point's "right" bezier handle.
* @param {Number} x3 - x position of second anchor point's "left" bezier handle.
* @param {Number} y3 - y position of second anchor point's "left" bezier handle.
* @param {Number} x4 - x position of second anchor point.
* @param {Number} y4 - y position of second anchor point.
* @returns {Object} Object contains min and max `x` / `y` bounds.
* @see {@link https://github.com/adobe-webplatform/Snap.svg/blob/master/src/path.js#L856}
*/
function getCurveBoundingBox(x1, y1, x2, y2, x3, y3, x4, y4) {
const tvalues = [];
const bounds = [[], []];
let a, b, c, t, t1, t2, b2ac, sqrtb2ac;
for (let i = 0; i < 2; ++i) {
if (i === 0) {
b = 6 * x1 - 12 * x2 + 6 * x3;
a = -3 * x1 + 9 * x2 - 9 * x3 + 3 * x4;
c = 3 * x2 - 3 * x1;
} else {
b = 6 * y1 - 12 * y2 + 6 * y3;
a = -3 * y1 + 9 * y2 - 9 * y3 + 3 * y4;
c = 3 * y2 - 3 * y1;
}
if (Math.abs(a) < 0.001) {
if (Math.abs(b) < 0.001) {
continue;
}
t = -c / b;
if (0 < t && t < 1) {
tvalues.push(t);
}
continue;
}
b2ac = b * b - 4 * c * a;
sqrtb2ac = Math.sqrt(b2ac);
if (b2ac < 0) {
continue;
}
t1 = (-b + sqrtb2ac) / (2 * a);
if (0 < t1 && t1 < 1) {
tvalues.push(t1);
}
t2 = (-b - sqrtb2ac) / (2 * a);
if (0 < t2 && t2 < 1) {
tvalues.push(t2);
}
}
let j = tvalues.length;
let jlen = j;
let mt;
while (j--) {
t = tvalues[j];
mt = 1 - t;
bounds[0][j] =
mt * mt * mt * x1 +
3 * mt * mt * t * x2 +
3 * mt * t * t * x3 +
t * t * t * x4;
bounds[1][j] =
mt * mt * mt * y1 +
3 * mt * mt * t * y2 +
3 * mt * t * t * y3 +
t * t * t * y4;
}
bounds[0][jlen] = x1;
bounds[1][jlen] = y1;
bounds[0][jlen + 1] = x4;
bounds[1][jlen + 1] = y4;
bounds[0].length = bounds[1].length = jlen + 2;
return {
min: { x: Math.min.apply(0, bounds[0]), y: Math.min.apply(0, bounds[1]) },
max: { x: Math.max.apply(0, bounds[0]), y: Math.max.apply(0, bounds[1]) },
};
}
/**
* @name Two.Utils.integrate
* @function
* @param {Function} f
* @param {Number} a
* @param {Number} b
* @param {Number} n
* @description Integration for `getCurveLength` calculations.
* @see [Paper.js](@link https://github.com/paperjs/paper.js/blob/master/src/util/Numerical.js#L101)
*/
function integrate(f, a, b, n) {
let x = Curve.abscissas[n - 2],
w = Curve.weights[n - 2],
A = 0.5 * (b - a),
B = A + a,
i = 0,
m = (n + 1) >> 1,
sum = n & 1 ? w[i++] * f(B) : 0; // Handle odd n
while (i < m) {
const Ax = A * x[i];
sum += w[i++] * (f(B + Ax) + f(B - Ax));
}
return A * sum;
}
/**
* @name Two.Utils.getCurveFromPoints
* @function
* @param {Anchor[]} points
* @param {Boolean} closed
* @description Sets the bezier handles on {@link Anchor}s in the `points` list with estimated values to create a catmull-rom like curve. Used by {@link Two.Path#plot}.
*/
function getCurveFromPoints(points, closed) {
const l = points.length,
last = l - 1;
for (let i = 0; i < l; i++) {
const point = points[i];
const prev = closed ? mod(i - 1, l) : Math.max(i - 1, 0);
const next = closed ? mod(i + 1, l) : Math.min(i + 1, last);
const a = points[prev];
const b = point;
const c = points[next];
getControlPoints(a, b, c);
b.command = i === 0 ? Commands.move : Commands.curve;
}
}
/**
* @name Two.Utils.getControlPoints
* @function
* @param {Anchor} a
* @param {Anchor} b
* @param {Anchor} c
* @returns {Anchor} Returns the passed middle point `b`.
* @description Given three coordinates set the control points for the middle, b, vertex based on its position with the adjacent points.
*/
function getControlPoints(a, b, c) {
const a1 = Vector.angleBetween(a, b);
const a2 = Vector.angleBetween(c, b);
let d1 = Vector.distanceBetween(a, b);
let d2 = Vector.distanceBetween(c, b);
let mid = (a1 + a2) / 2;
// TODO: Issue 73
if (d1 < 0.001 || d2 < 0.001) {
if (typeof b.relative === 'boolean' && !b.relative) {
b.controls.left.copy(b);
b.controls.right.copy(b);
}
return b;
}
d1 *= 0.33; // Why 0.33?
d2 *= 0.33;
if (a2 < a1) {
mid += HALF_PI;
} else {
mid -= HALF_PI;
}
b.controls.left.x = Math.cos(mid) * d1;
b.controls.left.y = Math.sin(mid) * d1;
mid -= Math.PI;
b.controls.right.x = Math.cos(mid) * d2;
b.controls.right.y = Math.sin(mid) * d2;
if (typeof b.relative === 'boolean' && !b.relative) {
b.controls.left.x += b.x;
b.controls.left.y += b.y;
b.controls.right.x += b.x;
b.controls.right.y += b.y;
}
return b;
}
/**
* @name Two.Utils.getReflection
* @function
* @param {Vector} a
* @param {Vector} b
* @param {Boolean} [relative=false]
* @returns {Vector} New {@link Vector} that represents the reflection point.
* @description Get the reflection of a point `b` about point `a`. Where `a` is in absolute space and `b` is relative to `a`.
* @see {@link http://www.w3.org/TR/SVG11/implnote.html#PathElementImplementationNotes}
*/
function getReflection(a, b, relative) {
return new Vector(
2 * a.x - (b.x + a.x) - (relative ? a.x : 0),
2 * a.y - (b.y + a.y) - (relative ? a.y : 0)
);
}
/**
* @name Two.Utils.getAnchorsFromArcData
* @function
* @param {Vector} center
* @param {Number} xAxisRotation
* @param {Number} rx - x radius
* @param {Number} ry - y radius
* @param {Number} ts
* @param {Number} td
* @param {Boolean} [ccw=false] - Set path traversal to counter-clockwise
*/
function getAnchorsFromArcData(center, xAxisRotation, rx, ry, ts, td, ccw) {
const resolution = Constants.Resolution;
const anchors = [];
for (let i = 0; i < resolution; i++) {
let pct = (i + 1) / resolution;
if (ccw) {
pct = 1 - pct;
}
const theta = pct * td + ts;
const x = rx * Math.cos(theta);
const y = ry * Math.sin(theta);
// x += center.x;
// y += center.y;
const anchor = new Anchor(x, y);
anchor.command = Commands.line;
// TODO: Calculate control points here...
anchors.push(anchor);
}
}
export {
Curve,
getComponentOnCubicBezier,
subdivide,
getCurveLength,
getCurveBoundingBox,
integrate,
getCurveFromPoints,
getControlPoints,
getReflection,
getAnchorsFromArcData,
};
================================================
FILE: src/utils/device-pixel-ratio.d.ts
================================================
declare module 'two.js/src/utils/device-pixel-ratio' {
/**
* @name Two.Utils.getRatio
* @function
* @param {CanvasRenderingContext2D} ctx
* @returns {Number} The ratio of a unit in Two.js to the pixel density of a session's screen.
* @see [High DPI Rendering](http://www.html5rocks.com/en/tutorials/canvas/hidpi/)
*/
export function getRatio(ctx: CanvasRenderingContext2D): number;
}
================================================
FILE: src/utils/device-pixel-ratio.js
================================================
import { root } from './root.js';
const devicePixelRatio = root.devicePixelRatio || 1;
function getBackingStoreRatio(ctx) {
return ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
}
/**
* @name Two.Utils.getRatio
* @function
* @param {CanvasRenderingContext2D} ctx
* @returns {Number} The ratio of a unit in Two.js to the pixel density of a session's screen.
* @see [High DPI Rendering](http://www.html5rocks.com/en/tutorials/canvas/hidpi/)
*/
function getRatio(ctx) {
return devicePixelRatio / getBackingStoreRatio(ctx);
}
export { getRatio };
================================================
FILE: src/utils/dom.d.ts
================================================
declare module 'two.js/src/utils/dom' {
export interface dom {
temp: HTMLDivElement;
}
}
================================================
FILE: src/utils/dom.js
================================================
import { root } from './root.js';
import { _ } from './underscore.js';
export const dom = {
hasEventListeners: typeof root.addEventListener === 'function',
bind: function(elem, event, func, bool) {
if (this.hasEventListeners) {
elem.addEventListener(event, func, !!bool);
} else {
elem.attachEvent('on' + event, func);
}
return dom;
},
unbind: function(elem, event, func, bool) {
if (dom.hasEventListeners) {
elem.removeEventListeners(event, func, !!bool);
} else {
elem.detachEvent('on' + event, func);
}
return dom;
},
getRequestAnimationFrame: function() {
const vendors = ['ms', 'moz', 'webkit', 'o'];
let lastTime = 0;
let request = root.requestAnimationFrame;
if (!request) {
for (let i = 0; i < vendors.length; i++) {
request = root[vendors[i] + 'RequestAnimationFrame'] || request;
}
request = request || fallbackRequest;
}
function fallbackRequest(callback, element) {
const currTime = new Date().getTime();
const timeToCall = Math.max(0, 16 - (currTime - lastTime));
const id = root.setTimeout(nextRequest, timeToCall);
lastTime = currTime + timeToCall;
function nextRequest() {
callback(currTime + timeToCall);
}
return id;
}
return request;
}
};
const temp = (root.document ? root.document.createElement('div') : {});
temp.id = 'help-two-load';
Object.defineProperty(dom, 'temp', {
enumerable: true,
get: function() {
if (_.isElement(temp) && !root.document.head.contains(temp)) {
temp.style.display = 'none';
root.document.head.appendChild(temp);
}
return temp;
}
});
================================================
FILE: src/utils/error.d.ts
================================================
declare module 'two.js/src/utils/error' {
/**
* @name Two.Utils.Error
* @class
* @description Custom error throwing for Two.js specific identification.
*/
export class TwoError extends Error {
constructor(message: string);
}
}
================================================
FILE: src/utils/error.js
================================================
/**
* @name Two.Utils.Error
* @class
* @description Custom error throwing for Two.js specific identification.
*/
export class TwoError extends Error {
name = 'Two.js';
message;
constructor(message) {
super();
this.message = message;
}
}
================================================
FILE: src/utils/hit-test.js
================================================
import { Commands } from './path-commands.js';
import { getSubdivisions } from './shape.js';
import { Constants } from '../constants.js';
import { mod, TWO_PI } from './math.js';
const TRANSPARENT_REGEX = /^(?:none|transparent)$/i;
const DEFAULT_PRECISION = 8;
const EPSILON = Number.EPSILON;
function createPoint(x, y) {
return { x, y };
}
function pointsEqual(a, b, epsilon = EPSILON) {
return Math.abs(a.x - b.x) <= epsilon && Math.abs(a.y - b.y) <= epsilon;
}
function svgAngle(ux, uy, vx, vy) {
const dot = ux * vx + uy * vy;
const len =
Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy) || 1e-12;
let ang = Math.acos(Math.max(-1, Math.min(1, dot / len)));
if (ux * vy - uy * vx < 0) {
ang = -ang;
}
return ang;
}
function sampleArcPoints(prev, anchor, precision) {
if (!prev) {
return [createPoint(anchor.x, anchor.y)];
}
let rx = anchor.rx;
let ry = anchor.ry;
if (!(rx && ry)) {
return [createPoint(anchor.x, anchor.y)];
}
const xAxisRotation = ((anchor.xAxisRotation || 0) * Math.PI) / 180;
const largeArcFlag = anchor.largeArcFlag ? 1 : 0;
const sweepFlag = anchor.sweepFlag ? 1 : 0;
rx = Math.abs(rx);
ry = Math.abs(ry);
const ax = prev.x;
const ay = prev.y;
const x = anchor.x;
const y = anchor.y;
const dx2 = (ax - x) / 2;
const dy2 = (ay - y) / 2;
const cosRot = Math.cos(xAxisRotation);
const sinRot = Math.sin(xAxisRotation);
let x1p = cosRot * dx2 + sinRot * dy2;
let y1p = -sinRot * dx2 + cosRot * dy2;
let rxs = rx * rx;
let rys = ry * ry;
const cr = (x1p * x1p) / rxs + (y1p * y1p) / rys;
if (cr > 1) {
const s = Math.sqrt(cr);
rx *= s;
ry *= s;
rxs = rx * rx;
rys = ry * ry;
}
const dq = rxs * y1p * y1p + rys * x1p * x1p;
const pq = dq === 0 ? 0 : (rxs * rys - dq) / dq;
let q = Math.sqrt(Math.max(0, pq));
if (largeArcFlag === sweepFlag) {
q = -q;
}
const cxp = (q * rx * y1p) / ry;
const cyp = (-q * ry * x1p) / rx;
const cx = cosRot * cxp - sinRot * cyp + (ax + x) / 2;
const cy = sinRot * cxp + cosRot * cyp + (ay + y) / 2;
const startAngle = svgAngle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
const delta =
svgAngle(
(x1p - cxp) / rx,
(y1p - cyp) / ry,
(-x1p - cxp) / rx,
(-y1p - cyp) / ry
) % TWO_PI;
const endAngle = startAngle + delta;
const clockwise = sweepFlag === 0;
const angleDelta = (() => {
const raw = endAngle - startAngle;
const samePoints = Math.abs(raw) < Number.EPSILON;
let deltaAngle = mod(raw, TWO_PI);
if (deltaAngle < Number.EPSILON) {
deltaAngle = samePoints ? 0 : TWO_PI;
}
if (clockwise && !samePoints) {
deltaAngle = deltaAngle === TWO_PI ? -TWO_PI : deltaAngle - TWO_PI;
}
return deltaAngle;
})();
const steps = Math.max(Constants.Resolution, Math.max(precision * 2, 1));
const points = [];
for (let i = 1; i <= steps; i++) {
const t = i / steps;
const angle = startAngle + t * angleDelta;
let px = cx + rx * Math.cos(angle);
let py = cy + ry * Math.sin(angle);
if (xAxisRotation !== 0) {
const tx = px - cx;
const ty = py - cy;
const cosR = Math.cos(xAxisRotation);
const sinR = Math.sin(xAxisRotation);
px = tx * cosR - ty * sinR + cx;
py = tx * sinR + ty * cosR + cy;
}
points.push(createPoint(px, py));
}
return points;
}
function buildPathHitParts(path, precision = DEFAULT_PRECISION) {
const polygons = [];
const segments = [];
const vertices =
path._renderer &&
path._renderer.vertices &&
path._renderer.vertices.length > 0
? path._renderer.vertices
: path.vertices;
if (!vertices || vertices.length === 0) {
return { polygons, segments };
}
const limit = Math.max(1, Math.floor(precision));
let currentPolygon = null;
let firstPoint = null;
let lastPoint = null;
let prevVertex = null;
const closePolygon = (forceClose = false) => {
if (!currentPolygon) {
return;
}
if (
forceClose &&
firstPoint &&
lastPoint &&
!pointsEqual(firstPoint, lastPoint)
) {
const closingPoint = createPoint(firstPoint.x, firstPoint.y);
segments.push({ a: lastPoint, b: closingPoint });
currentPolygon.push(closingPoint);
lastPoint = closingPoint;
}
if (
currentPolygon.length >= 3 &&
firstPoint &&
lastPoint &&
pointsEqual(firstPoint, lastPoint)
) {
polygons.push(currentPolygon);
}
currentPolygon = null;
firstPoint = null;
lastPoint = null;
};
const appendPoint = (pt) => {
if (!lastPoint) {
lastPoint = pt;
if (currentPolygon) {
currentPolygon.push(pt);
}
return;
}
if (pointsEqual(lastPoint, pt)) {
return;
}
segments.push({ a: lastPoint, b: pt });
if (currentPolygon) {
currentPolygon.push(pt);
}
lastPoint = pt;
};
for (let i = 0; i < vertices.length; i++) {
const vertex = vertices[i];
const command = vertex.command || (i === 0 ? Commands.move : Commands.line);
if (command === Commands.move) {
closePolygon(false);
const pt = createPoint(vertex.x, vertex.y);
currentPolygon = [pt];
firstPoint = pt;
lastPoint = pt;
prevVertex = vertex;
continue;
}
if (!prevVertex) {
prevVertex = vertices[Math.max(i - 1, 0)];
}
if (command === Commands.line) {
appendPoint(createPoint(vertex.x, vertex.y));
} else if (command === Commands.curve) {
const subdivisions = getSubdivisions(vertex, prevVertex, limit);
for (let j = 1; j < subdivisions.length; j++) {
const sv = subdivisions[j];
appendPoint(createPoint(sv.x, sv.y));
}
appendPoint(createPoint(vertex.x, vertex.y));
} else if (command === Commands.arc) {
const arcPoints = sampleArcPoints(prevVertex, vertex, limit);
for (let j = 0; j < arcPoints.length; j++) {
appendPoint(arcPoints[j]);
}
} else if (command === Commands.close) {
closePolygon(true);
prevVertex = vertex;
continue;
} else {
appendPoint(createPoint(vertex.x, vertex.y));
}
prevVertex = vertex;
}
if (currentPolygon) {
const shouldForceClose =
!!path._closed ||
!!path.closed ||
(firstPoint && lastPoint && !pointsEqual(firstPoint, lastPoint));
closePolygon(shouldForceClose);
}
return { polygons, segments };
}
function pointInPolygons(polygons, x, y) {
let inside = false;
for (let i = 0; i < polygons.length; i++) {
const polygon = polygons[i];
if (!polygon || polygon.length < 3) {
continue;
}
let lastIndex = polygon.length - 1;
for (let j = 0; j < polygon.length; j++) {
const v0 = polygon[lastIndex];
const v1 = polygon[j];
const intersects =
v1.y > y !== v0.y > y &&
x < ((v0.x - v1.x) * (y - v1.y)) / (v0.y - v1.y || 1e-12) + v1.x;
if (intersects) {
inside = !inside;
}
lastIndex = j;
}
}
return inside;
}
function distanceToSegmentSquared(x, y, a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
const ddx = x - a.x;
const ddy = y - a.y;
return ddx * ddx + ddy * ddy;
}
const t = ((x - a.x) * dx + (y - a.y) * dy) / (dx * dx + dy * dy);
const clamped = Math.max(0, Math.min(1, t));
const cx = a.x + clamped * dx;
const cy = a.y + clamped * dy;
const ddx = x - cx;
const ddy = y - cy;
return ddx * ddx + ddy * ddy;
}
function distanceToSegments(segments, x, y) {
if (!segments || segments.length === 0) {
return Infinity;
}
let minDistance = Infinity;
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const distance = distanceToSegmentSquared(x, y, segment.a, segment.b);
if (distance < minDistance) {
minDistance = distance;
}
}
return Math.sqrt(minDistance);
}
function hasVisibleFill(shape, override) {
if (typeof override === 'boolean') {
return override;
}
const fill = shape.fill;
if (!fill && fill !== 0) {
return false;
}
if (typeof fill === 'string') {
return !TRANSPARENT_REGEX.test(fill);
}
return true;
}
function hasVisibleStroke(shape, override) {
const linewidth =
typeof shape.linewidth === 'number'
? shape.linewidth
: shape._linewidth || 0;
if (typeof override === 'boolean') {
return override && linewidth > 0;
}
if (!(linewidth > 0)) {
return false;
}
const stroke = shape.stroke;
if (!stroke && stroke !== 0) {
return false;
}
if (typeof stroke === 'string') {
return !TRANSPARENT_REGEX.test(stroke);
}
return true;
}
function boundsContains(rect, x, y, tolerance = 0) {
if (!rect) {
return false;
}
const left = rect.left - tolerance;
const right = rect.right + tolerance;
const top = rect.top - tolerance;
const bottom = rect.bottom + tolerance;
return x >= left && x <= right && y >= top && y <= bottom;
}
export {
buildPathHitParts,
pointInPolygons,
distanceToSegments,
hasVisibleFill,
hasVisibleStroke,
boundsContains,
};
================================================
FILE: src/utils/interpret-svg.d.ts
================================================
declare module 'two.js/src/utils/interpret-svg' {
/**
* @name Two.Utils.read
* @property {Object} read - A map of functions to read any number of SVG node types and create Two.js equivalents of them. Primarily used by the {@link Two#interpret} method.
*/
export const read: {
svg: (node: any) => any;
defs: (node: any) => any;
use: (node: any, styles: any) => any;
g: (node: any, parentStyles: any) => Group;
polygon: (node: any, parentStyles: any) => Path;
polyline: (node: any, parentStyles: any) => any;
path: (node: any, parentStyles: any) => Path;
circle: (node: any, parentStyles: any) => Circle;
ellipse: (node: any, parentStyles: any) => Ellipse;
rect: (node: any, parentStyles: any) => Rectangle | RoundedRectangle;
'rounded-rect': (node: any, parentStyles: any) => RoundedRectangle;
line: (node: any, parentStyles: any) => Line;
lineargradient: (node: any, parentStyles: any) => LinearGradient;
radialgradient: (node: any, parentStyles: any) => RadialGradient;
text: (node: any, parentStyles: any) => Text;
clippath: (node: any, parentStyles: any) => any;
image: (node: any, parentStyles: any) => Sprite;
};
import { Group } from 'two.js/src/group';
import { Path } from 'two.js/src/path';
import { Circle } from 'two.js/src/shapes/circle';
import { Ellipse } from 'two.js/src/shapes/ellipse';
import { Rectangle } from 'two.js/src/shapes/rectangle';
import { RoundedRectangle } from 'two.js/src/shapes/rounded-rectangle';
import { Line } from 'two.js/src/shapes/line';
import { LinearGradient } from 'two.js/src/effects/linear-gradient';
import { RadialGradient } from 'two.js/src/effects/radial-gradient';
import { Text } from 'two.js/src/text';
import { Sprite } from 'two.js/src/effects/sprite';
}
================================================
FILE: src/utils/interpret-svg.js
================================================
import { root } from './root.js';
import { Commands } from './path-commands.js';
import { decomposeMatrix } from './math.js';
import { getReflection } from './curves.js';
import { _ } from './underscore.js';
import { TwoError } from './error.js';
import { Registry } from '../registry.js';
import { Anchor } from '../anchor.js';
import { Vector } from '../vector.js';
import { Path } from '../path.js';
import { Sprite } from '../effects/sprite.js';
import { Group } from '../group.js';
import { Circle } from '../shapes/circle.js';
import { Ellipse } from '../shapes/ellipse.js';
import { Line } from '../shapes/line.js';
import { Rectangle } from '../shapes/rectangle.js';
import { RoundedRectangle } from '../shapes/rounded-rectangle.js';
import { Stop } from '../effects/stop.js';
import { Gradient } from '../effects/gradient.js';
import { LinearGradient } from '../effects/linear-gradient.js';
import { RadialGradient } from '../effects/radial-gradient.js';
import { Text } from '../text.js';
import { Constants } from '../constants.js';
// https://github.com/jonobr1/two.js/issues/507#issuecomment-777159213
const regex = {
path: /[+-]?(?:\d*\.\d+|\d+)(?:[eE][+-]\d+)?/g,
cssBackgroundImage: /url\(['"]?#([\w\d-_]*)['"]?\)/i,
unitSuffix: /[a-zA-Z%]*/i,
};
const alignments = {
start: 'left',
middle: 'center',
end: 'right',
};
// Reserved attributes to remove
const reservedAttributesToRemove = [
'id',
'class',
'transform',
'xmlns',
'viewBox',
];
const overwriteAttrs = ['x', 'y', 'width', 'height', 'href', 'xlink:href'];
/**
* @name Two.Utils.getAlignment
* @function
* @param {AlignmentString}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor}
*/
function getAlignment(anchor) {
return alignments[anchor];
}
function getBaseline(node) {
const a = node.getAttribute('dominant-baseline');
const b = node.getAttribute('alignment-baseline');
return a || b;
}
function getTagName(tag) {
return tag.replace(/svg:/gi, '').toLowerCase();
}
function applyTransformsToVector(transforms, vector) {
vector.x += transforms.translateX;
vector.y += transforms.translateY;
vector.x *= transforms.scaleX;
vector.y *= transforms.scaleY;
if (transforms.rotation !== 0) {
// TODO: Test further
const l = vector.length();
vector.x = l * Math.cos(transforms.rotation);
vector.y = l * Math.sin(transforms.rotation);
}
}
/**
* @name Two.Utils.extractCSSText
* @function
* @param {String} text - The CSS text body to be parsed and extracted.
* @param {Object} [styles] - The styles object to apply CSS key values to.
* @returns {Object} styles
* @description Parse CSS text body and apply them as key value pairs to a JavaScript object.
*/
function extractCSSText(text, styles) {
if (!styles) {
styles = {};
}
const commands = text.split(';');
for (let i = 0; i < commands.length; i++) {
const command = commands[i].split(':');
const name = command[0];
const value = command[1];
if (typeof name === 'undefined' || typeof value === 'undefined') {
continue;
}
//Delete whitespace and line breaks from name and value
const trimmedName = name.replace(/\s/g, '');
const trimmedValue = value.replace(/\s/g, '');
styles[trimmedName] = trimmedValue;
}
return styles;
}
/**
* @name Two.Utils.getSvgStyles
* @function
* @param {SVGElement} node - The SVG node to parse.
* @returns {Object} styles
* @description Get the CSS comands from the `style` attribute of an SVG node and apply them as key value pairs to a JavaScript object.
*/
function getSvgStyles(node) {
const styles = {};
const attributes = getSvgAttributes(node);
const length = Math.max(attributes.length, node.style.length);
for (let i = 0; i < length; i++) {
const command = node.style[i];
const attribute = attributes[i];
if (command) {
styles[command] = node.style[command];
}
if (attribute) {
styles[attribute] = node.getAttribute(attribute);
}
}
return styles;
}
function getSvgAttributes(node) {
const attributes = node.getAttributeNames();
for (let i = 0; i < reservedAttributesToRemove.length; i++) {
const keyword = reservedAttributesToRemove[i];
const index = Array.prototype.indexOf.call(attributes, keyword);
if (index >= 0) {
attributes.splice(index, 1);
}
}
return attributes;
}
/**
* @name Two.Utils.applySvgViewBox
* @function
* @param {Two.Shape} node - The Two.js object to apply viewbox matrix to
* @param {String} value - The viewBox value from the SVG attribute
* @returns {Two.Shape} node
* @description Applies the transform of the SVG Viewbox on a given node.
*/
function applySvgViewBox(node, value) {
const elements = value.split(/[\s,]/);
const x = -parseFloat(elements[0]);
const y = -parseFloat(elements[1]);
const width = parseFloat(elements[2]);
const height = parseFloat(elements[3]);
if (x && y) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if ('translation' in child) {
child.translation.add(x, y);
} else if ('x' in child) {
child.x = x;
} else if ('y' in child) {
child.y = y;
}
}
}
const xExists = typeof node.x === 'number';
const yExists = typeof node.y === 'number';
const widthExists = typeof node.width === 'number';
const heightExists = typeof node.height === 'number';
if (xExists) {
node.translation.x += node.x;
}
if (yExists) {
node.translation.y += node.y;
}
if (widthExists || heightExists) {
node.scale = new Vector(1, 1);
}
if (widthExists) {
node.scale.x = node.width / width;
}
if (heightExists) {
node.scale.y = node.height / height;
}
node.mask = new Rectangle(0, 0, width, height);
node.mask.origin.set(-width / 2, -height / 2);
return node;
}
/**
* @name Two.Utils.applySvgAttributes
* @function
* @param {SVGElement} node - An SVG Node to extrapolate attributes from.
* @param {Two.Shape} elem - The Two.js object to apply extrapolated attributes to.
* @returns {Two.Shape} The Two.js object passed now with applied attributes.
* @description This function iterates through an SVG Node's properties and stores ones of interest. It tries to resolve styles applied via CSS as well.
* @TODO Reverse calculate {@link Two.Gradient}s for fill / stroke of any given path.
*/
function applySvgAttributes(node, elem, parentStyles) {
const styles = {},
attributes = {},
extracted = {};
let i, m, key, value, prop, attr;
let transforms, x, y;
let id, scene, ref, tagName;
let ca, cb, cc, error;
if (node === null) {
return styles;
}
// Not available in non browser environments
if (root.getComputedStyle) {
// Convert CSSStyleDeclaration to a normal object
const computedStyles = root.getComputedStyle(node);
i = computedStyles.length;
while (i--) {
key = computedStyles[i];
value = computedStyles[key];
// Gecko returns undefined for unset properties
// Webkit returns the default value
if (typeof value !== 'undefined') {
styles[key] = value;
}
}
}
// Convert NodeMap to a normal object
for (i = 0; i < node.attributes.length; i++) {
attr = node.attributes[i];
if (/style/i.test(attr.nodeName)) {
extractCSSText(attr.value, extracted);
} else {
attributes[attr.nodeName] = attr.value;
}
}
// Getting the correct opacity is a bit tricky, since SVG path elements don't
// support opacity as an attribute, but you can apply it via CSS.
// So we take the opacity and set (stroke/fill)-opacity to the same value.
if (typeof styles.opacity !== 'undefined') {
styles['stroke-opacity'] = styles.opacity;
styles['fill-opacity'] = styles.opacity;
delete styles.opacity;
}
// Merge attributes and applied styles (attributes take precedence)
if (parentStyles) {
_.defaults(styles, parentStyles);
}
_.extend(styles, extracted, attributes);
// Similarly visibility is influenced by the value of both display and visibility.
// Calculate a unified value here which defaults to `true`.
styles.visible =
!(typeof styles.display === 'undefined' && /none/i.test(styles.display)) ||
(typeof styles.visibility === 'undefined' &&
/hidden/i.test(styles.visibility));
// Now iterate the whole thing
for (key in styles) {
value = styles[key];
switch (key) {
case 'gradientTransform':
// TODO: Check this out https://github.com/paperjs/paper.js/blob/develop/src/svg/SvgImport.js#L315
if (/none/i.test(value)) break;
m =
node.gradientTransform &&
node.gradientTransform.baseVal &&
node.gradientTransform.baseVal.length > 0
? node.gradientTransform.baseVal[0].matrix
: node.getCTM
? node.getCTM()
: null;
if (m === null) break;
transforms = decomposeMatrix(m);
switch (elem._renderer.type) {
case 'linear-gradient':
applyTransformsToVector(transforms, elem.left);
applyTransformsToVector(transforms, elem.right);
break;
case 'radial-gradient':
elem.center.x += transforms.translateX;
elem.center.y += transforms.translateY;
elem.focal.x += transforms.translateX;
elem.focal.y += transforms.translateY;
elem.radius *= Math.max(transforms.scaleX, transforms.scaleY);
break;
}
break;
case 'transform':
// TODO: Check this out https://github.com/paperjs/paper.js/blob/develop/src/svg/SvgImport.js#L315
if (/none/i.test(value)) break;
m =
node.transform &&
node.transform.baseVal &&
node.transform.baseVal.length > 0
? node.transform.baseVal[0].matrix
: node.getCTM
? node.getCTM()
: null;
// Might happen when transform string is empty or not valid.
if (m === null) break;
if (Constants.AutoCalculateImportedMatrices) {
// Decompose and infer Two.js related properties.
transforms = decomposeMatrix(m);
elem.translation.set(transforms.translateX, transforms.translateY);
elem.rotation = Math.PI * (transforms.rotation / 180);
elem.scale = new Vector(transforms.scaleX, transforms.scaleY);
x = parseFloat((styles.x + '').replace('px'));
y = parseFloat((styles.y + '').replace('px'));
// Override based on attributes.
if (x) {
elem.translation.x = x;
}
if (y) {
elem.translation.y = y;
}
} else {
// Edit the underlying matrix and don't force an auto calc.
m = node.getCTM();
elem._matrix.manual = true;
elem._matrix.set(m.a, m.b, m.c, m.d, m.e, m.f);
}
break;
case 'visible':
if (elem instanceof Group) {
elem._visible = value;
break;
}
elem.visible = value;
break;
case 'stroke-linecap':
if (elem instanceof Group) {
elem._cap = value;
break;
}
elem.cap = value;
break;
case 'stroke-linejoin':
if (elem instanceof Group) {
elem._join = value;
break;
}
elem.join = value;
break;
case 'stroke-miterlimit':
if (elem instanceof Group) {
elem._miter = value;
break;
}
elem.miter = value;
break;
case 'stroke-width':
if (elem instanceof Group) {
elem._linewidth = parseFloat(value);
break;
}
elem.linewidth = parseFloat(value);
break;
case 'opacity':
case 'stroke-opacity':
case 'fill-opacity':
// Only apply styles to rendered shapes
// in the scene.
if (elem instanceof Group) {
elem._opacity = parseFloat(value);
break;
}
elem.opacity = parseFloat(value);
break;
case 'clip-path':
if (regex.cssBackgroundImage.test(value)) {
id = value.replace(regex.cssBackgroundImage, '$1');
if (read.defs.current && read.defs.current.contains(id)) {
ref = read.defs.current.get(id);
if (ref && ref.childNodes.length > 0) {
ref = ref.childNodes[0];
tagName = getTagName(ref.nodeName);
elem.mask = read[tagName].call(this, ref, {});
switch (elem._renderer.type) {
case 'text':
case 'path':
// The matrix here needs to change to insure that the object
// clipping is in the same coordinate space as the `elem`.
elem.position.add(elem.mask.position);
elem.mask.position.clear();
break;
}
}
}
}
break;
case 'fill':
case 'stroke':
prop = (elem instanceof Group ? '_' : '') + key;
if (regex.cssBackgroundImage.test(value)) {
id = value.replace(regex.cssBackgroundImage, '$1');
// Overwritten id for non-conflicts on same page SVG documents
// TODO: Make this non-descructive
// node.setAttribute('two-' + key, value.replace(/\)/i, '-' + Constants.Identifier + 'applied)'));
if (read.defs.current && read.defs.current.contains(id)) {
ref = read.defs.current.get(id);
if (!ref.object) {
tagName = getTagName(ref.nodeName);
ref.object = read[tagName].call(this, ref, {});
}
ref = ref.object;
} else {
scene = getScene(this);
ref = scene.getById(id);
}
elem[prop] = ref;
} else {
elem[prop] = value;
}
break;
case 'id':
elem.id = value;
// Overwritten id for non-conflicts on same page SVG documents
// TODO: Make this non-descructive
// node.id = value + '-' + Constants.Identifier + 'applied';
break;
case 'class':
case 'className':
elem.classList = value.split(' ');
elem._flagClassName = true;
break;
case 'x':
case 'y':
ca = elem instanceof Gradient;
cb = elem instanceof LinearGradient;
cc = elem instanceof RadialGradient;
if (ca || cb || cc) {
break;
}
if (value.match('[a-z%]$') && !value.endsWith('px')) {
error = new TwoError(
'only pixel values are supported with the ' + key + ' attribute.'
);
console.warn(error.name, error.message);
}
elem.translation[key] = parseFloat(value);
break;
case 'font-family':
if (elem instanceof Text) {
elem.family = value;
}
break;
case 'font-size':
if (elem instanceof Text) {
if (value.match('[a-z%]$') && !value.endsWith('px')) {
error = new TwoError(
'only pixel values are supported with the ' + key + ' attribute.'
);
console.warn(error.name, error.message);
}
elem.size = parseFloat(value);
}
break;
case 'font-weight':
if (elem instanceof Text) {
elem.weight = value;
}
break;
case 'font-style':
if (elem instanceof Text) {
elem.style = value;
}
break;
case 'text-decoration':
if (elem instanceof Text) {
elem.decoration = value;
}
break;
case 'line-height':
if (elem instanceof Text) {
elem.leading = value;
}
break;
}
}
if (Object.keys(node.dataset).length) elem.dataset = node.dataset;
return styles;
}
/**
* @name Two.Utils.updateDefsCache
* @function
* @param {SVGElement} node - The SVG Node with which to update the defs cache.
* @param {Object} Object - The defs cache to be updated.
* @description Update the cache of children of tags.
*/
function updateDefsCache(node, defsCache) {
for (let i = 0, l = node.childNodes.length; i < l; i++) {
const n = node.childNodes[i];
if (!n.id) continue;
const tagName = getTagName(node.nodeName);
if (tagName === '#text') continue;
defsCache.add(n.id, n);
}
}
/**
* @name Two.Utils.getScene
* @param {Two.Shape} node - The currently available object in the scenegraph.
* @returns {Group} - The highest order {@link Two.Group} in the scenegraph.
* @property {Function}
*/
function getScene(node) {
while (node.parent) {
node = node.parent;
}
return node.scene;
}
/**
* @name Two.Utils.read
* @property {Object} read - A map of functions to read any number of SVG node types and create Two.js equivalents of them. Primarily used by the {@link Two#interpret} method.
*/
export const read = {
svg: function (node) {
const defs = (read.defs.current = new Registry());
const elements = node.getElementsByTagName('defs');
for (let i = 0; i < elements.length; i++) {
updateDefsCache(elements[i], defs);
}
const svg = read.g.call(this, node);
const viewBox = node.getAttribute('viewBox');
const x = node.getAttribute('x');
const y = node.getAttribute('y');
const width = node.getAttribute('width');
const height = node.getAttribute('height');
svg.defs = defs; // Export out the for later use
const viewBoxExists = viewBox !== null;
const xExists = x !== null;
const yExists = y !== null;
const widthExists = width !== null;
const heightExists = height !== null;
if (xExists) {
svg.x = parseFloat(x.replace(regex.unitSuffix, ''));
}
if (yExists) {
svg.y = parseFloat(y.replace(regex.unitSuffix, ''));
}
if (widthExists) {
svg.width = parseFloat(width.replace(regex.unitSuffix, ''));
}
if (heightExists) {
svg.height = parseFloat(height.replace(regex.unitSuffix, ''));
}
if (viewBoxExists) {
applySvgViewBox(svg, viewBox);
}
delete read.defs.current;
return svg;
},
defs: function (node) {
return null;
},
use: function (node, styles) {
let error;
const href = node.getAttribute('href') || node.getAttribute('xlink:href');
if (!href) {
error = new TwoError('encountered with no href.');
console.warn(error.name, error.message);
return null;
}
const id = href.slice(1);
if (!read.defs.current.contains(id)) {
error = new TwoError(
'unable to find element for reference ' + href + '.'
);
console.warn(error.name, error.message);
return null;
}
const template = read.defs.current.get(id);
const fullNode = template.cloneNode(true);
for (let i = 0; i < node.attributes.length; i++) {
const attr = node.attributes[i];
const ca = overwriteAttrs.includes(attr.nodeName);
const cb = !fullNode.hasAttribute(attr.nodeName);
if (ca || cb) {
fullNode.setAttribute(attr.nodeName, attr.value);
}
}
const tagName = getTagName(fullNode.nodeName);
return read[tagName].call(this, fullNode, styles);
},
g: function (node, parentStyles) {
const group = new Group();
applySvgAttributes.call(this, node, group, parentStyles);
this.add(group);
// Switched up order to inherit more specific styles
const styles = getSvgStyles.call(this, node);
for (let i = 0, l = node.childNodes.length; i < l; i++) {
const n = node.childNodes[i];
const tag = n.nodeName;
if (!tag) return;
const tagName = getTagName(tag);
if (tagName in read) {
const o = read[tagName].call(group, n, styles);
if (!!o && !o.parent) {
group.add(o);
}
}
}
return group;
},
polygon: function (node, parentStyles) {
let points;
if (typeof node === 'string') {
points = node;
} else {
points = node.getAttribute('points');
}
const verts = [];
points.replace(
/(-?[\d.eE-]+)[,|\s](-?[\d.eE-]+)/g,
function (match, p1, p2) {
verts.push(new Anchor(parseFloat(p1), parseFloat(p2)));
}
);
const poly = new Path(verts, true);
poly.stroke = 'none';
poly.fill = 'black';
applySvgAttributes.call(this, node, poly, parentStyles);
return poly;
},
polyline: function (node, parentStyles) {
const poly = read.polygon.call(this, node, parentStyles);
poly.closed = false;
return poly;
},
path: function (node, parentStyles) {
let path;
if (typeof node === 'string') {
path = node;
node = null;
} else {
path = node.getAttribute('d');
}
let points = [];
let closed = false,
relative = false;
if (path) {
// Create a Two.Path from the paths.
let coord = new Anchor();
let control, coords;
let commands = path.match(/[a-df-z][^a-df-z]*/gi);
const last = commands.length - 1;
// Split up polybeziers
_.each(commands.slice(0), function (command, i) {
const items = command.slice(1).trim().match(regex.path);
const type = command[0];
const lower = type.toLowerCase();
let bin, j, l, ct, times;
const result = [];
if (i === 0) {
commands = [];
}
switch (lower) {
case 'h':
case 'v':
if (items.length > 1) {
bin = 1;
}
break;
case 'm':
case 'l':
case 't':
if (items.length > 2) {
bin = 2;
}
break;
case 's':
case 'q':
if (items.length > 4) {
bin = 4;
}
break;
case 'c':
if (items.length > 6) {
bin = 6;
}
break;
case 'a':
if (items.length > 7) {
bin = 7;
}
break;
}
// This means we have a polybezier.
if (bin) {
for (j = 0, l = items.length, times = 0; j < l; j += bin) {
ct = type;
if (times > 0) {
switch (type) {
case 'm':
ct = 'l';
break;
case 'M':
ct = 'L';
break;
}
}
result.push(ct + items.slice(j, j + bin).join(' '));
times++;
}
commands = Array.prototype.concat.apply(commands, result);
} else {
commands.push(command);
}
});
// Create the vertices for our Two.Path
_.each(commands, function (command, i) {
let result, x, y;
const type = command[0];
const lower = type.toLowerCase();
coords = command.slice(1).trim().match(regex.path);
relative = type === lower;
let x1, y1, x2, y2, x3, y3, x4, y4, reflection;
let a, b;
let anchor, rx, ry, xAxisRotation, largeArcFlag, sweepFlag;
switch (lower) {
case 'z':
if (i >= last) {
closed = true;
} else {
x = coord.x;
y = coord.y;
result = new Anchor(
x,
y,
undefined,
undefined,
undefined,
undefined,
Commands.close
);
// Make coord be the last `m` command
for (let j = points.length - 1; j >= 0; j--) {
const point = points[j];
if (/m/i.test(point.command)) {
coord = point;
break;
}
}
}
break;
case 'm':
case 'l':
control = undefined;
x = parseFloat(coords[0]);
y = parseFloat(coords[1]);
result = new Anchor(
x,
y,
undefined,
undefined,
undefined,
undefined,
/m/i.test(lower) ? Commands.move : Commands.line
);
if (relative) {
result.addSelf(coord);
}
// result.controls.left.copy(result);
// result.controls.right.copy(result);
coord = result;
break;
case 'h':
case 'v':
a = /h/i.test(lower) ? 'x' : 'y';
b = /x/i.test(a) ? 'y' : 'x';
result = new Anchor(
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
Commands.line
);
result[a] = parseFloat(coords[0]);
result[b] = coord[b];
if (relative) {
result[a] += coord[a];
}
// result.controls.left.copy(result);
// result.controls.right.copy(result);
coord = result;
break;
case 'c':
case 's':
x1 = coord.x;
y1 = coord.y;
if (!control) {
control = new Vector(); //.copy(coord);
}
if (/c/i.test(lower)) {
x2 = parseFloat(coords[0]);
y2 = parseFloat(coords[1]);
x3 = parseFloat(coords[2]);
y3 = parseFloat(coords[3]);
x4 = parseFloat(coords[4]);
y4 = parseFloat(coords[5]);
} else {
// Calculate reflection control point for proper x2, y2
// inclusion.
reflection = getReflection(coord, control, relative);
x2 = reflection.x;
y2 = reflection.y;
x3 = parseFloat(coords[0]);
y3 = parseFloat(coords[1]);
x4 = parseFloat(coords[2]);
y4 = parseFloat(coords[3]);
}
if (relative) {
x2 += x1;
y2 += y1;
x3 += x1;
y3 += y1;
x4 += x1;
y4 += y1;
}
coord.controls.right.set(x2 - coord.x, y2 - coord.y);
result = new Anchor(
x4,
y4,
x3 - x4,
y3 - y4,
undefined,
undefined,
Commands.curve
);
coord = result;
control = result.controls.left;
break;
case 't':
case 'q':
x1 = coord.x;
y1 = coord.y;
if (!control) {
control = new Vector();
}
if (/q/i.test(lower)) {
x2 = parseFloat(coords[0]);
y2 = parseFloat(coords[1]);
x3 = parseFloat(coords[0]);
y3 = parseFloat(coords[1]);
x4 = parseFloat(coords[2]);
y4 = parseFloat(coords[3]);
} else {
reflection = getReflection(coord, control, relative);
x2 = reflection.x;
y2 = reflection.y;
x3 = reflection.x;
y3 = reflection.y;
x4 = parseFloat(coords[0]);
y4 = parseFloat(coords[1]);
}
if (relative) {
x2 += x1;
y2 += y1;
x3 += x1;
y3 += y1;
x4 += x1;
y4 += y1;
}
coord.controls.right.set(
(x2 - coord.x) * 0.33,
(y2 - coord.y) * 0.33
);
result = new Anchor(
x4,
y4,
x3 - x4,
y3 - y4,
undefined,
undefined,
Commands.curve
);
coord = result;
control = result.controls.left;
break;
case 'a':
x1 = coord.x;
y1 = coord.y;
rx = parseFloat(coords[0]);
ry = parseFloat(coords[1]);
xAxisRotation = parseFloat(coords[2]); // * PI / 180;
largeArcFlag = parseFloat(coords[3]);
sweepFlag = parseFloat(coords[4]);
x4 = parseFloat(coords[5]);
y4 = parseFloat(coords[6]);
if (relative) {
x4 += x1;
y4 += y1;
}
anchor = new Anchor(x4, y4);
anchor.command = Commands.arc;
anchor.rx = rx;
anchor.ry = ry;
anchor.xAxisRotation = xAxisRotation;
anchor.largeArcFlag = largeArcFlag;
anchor.sweepFlag = sweepFlag;
result = anchor;
coord = anchor;
control = undefined;
break;
}
if (result) {
if (Array.isArray(result)) {
points = points.concat(result);
} else {
points.push(result);
}
}
});
}
path = new Path(points, closed, undefined, true);
path.stroke = 'none';
path.fill = 'black';
const rect = path.getBoundingClientRect(true);
// Center objects to stay consistent
// with the rest of the Two.js API.
rect.centroid = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
};
_.each(path.vertices, function (v) {
v.subSelf(rect.centroid);
});
applySvgAttributes.call(this, node, path, parentStyles);
path.translation.addSelf(rect.centroid);
return path;
},
circle: function (node, parentStyles) {
const x = parseFloat(node.getAttribute('cx'));
const y = parseFloat(node.getAttribute('cy'));
const r = parseFloat(node.getAttribute('r'));
const circle = new Circle(0, 0, r);
circle.stroke = 'none';
circle.fill = 'black';
applySvgAttributes.call(this, node, circle, parentStyles);
circle.translation.x = x;
circle.translation.y = y;
return circle;
},
ellipse: function (node, parentStyles) {
const x = parseFloat(node.getAttribute('cx'));
const y = parseFloat(node.getAttribute('cy'));
const width = parseFloat(node.getAttribute('rx'));
const height = parseFloat(node.getAttribute('ry'));
const ellipse = new Ellipse(0, 0, width, height);
ellipse.stroke = 'none';
ellipse.fill = 'black';
applySvgAttributes.call(this, node, ellipse, parentStyles);
ellipse.translation.x = x;
ellipse.translation.y = y;
return ellipse;
},
rect: function (node, parentStyles) {
const rx = parseFloat(node.getAttribute('rx'));
const ry = parseFloat(node.getAttribute('ry'));
if (!_.isNaN(rx) || !_.isNaN(ry)) {
return read['rounded-rect'](node);
}
const width = parseFloat(node.getAttribute('width'));
const height = parseFloat(node.getAttribute('height'));
const w2 = width / 2;
const h2 = height / 2;
const rect = new Rectangle(0, 0, width, height);
rect.stroke = 'none';
rect.fill = 'black';
applySvgAttributes.call(this, node, rect, parentStyles);
// For rectangles, (x, y) is the center of the shape rather than the top
// left corner.
rect.translation.x += w2;
rect.translation.y += h2;
return rect;
},
'rounded-rect': function (node, parentStyles) {
const rx = parseFloat(node.getAttribute('rx')) || 0;
const ry = parseFloat(node.getAttribute('ry')) || 0;
const width = parseFloat(node.getAttribute('width'));
const height = parseFloat(node.getAttribute('height'));
const w2 = width / 2;
const h2 = height / 2;
const radius = new Vector(rx, ry);
const rect = new RoundedRectangle(0, 0, width, height, radius);
rect.stroke = 'none';
rect.fill = 'black';
applySvgAttributes.call(this, node, rect, parentStyles);
// For rectangles, (x, y) is the center of the shape rather than the top
// left corner.
rect.translation.x += w2;
rect.translation.y += h2;
return rect;
},
line: function (node, parentStyles) {
const x1 = parseFloat(node.getAttribute('x1'));
const y1 = parseFloat(node.getAttribute('y1'));
const x2 = parseFloat(node.getAttribute('x2'));
const y2 = parseFloat(node.getAttribute('y2'));
const line = new Line(x1, y1, x2, y2).noFill();
applySvgAttributes.call(this, node, line, parentStyles);
return line;
},
lineargradient: function (node, parentStyles) {
let units = node.getAttribute('gradientUnits');
let spread = node.getAttribute('spreadMethod');
if (!units) {
units = 'objectBoundingBox';
}
if (!spread) {
spread = 'pad';
}
let x1 = parseFloat(node.getAttribute('x1') || 0);
let y1 = parseFloat(node.getAttribute('y1') || 0);
let x2 = parseFloat(node.getAttribute('x2') || 0);
let y2 = parseFloat(node.getAttribute('y2') || 0);
const ox = (x2 + x1) / 2;
const oy = (y2 + y1) / 2;
if (/userSpaceOnUse/i.test(units)) {
x1 -= ox;
y1 -= oy;
x2 -= ox;
y2 -= oy;
}
const stops = [];
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
let offset = child.getAttribute('offset');
if (/%/gi.test(offset)) {
offset = parseFloat(offset.replace(/%/gi, '')) / 100;
}
offset = parseFloat(offset);
let color = child.getAttribute('stop-color');
let opacity = child.getAttribute('stop-opacity');
let style = child.getAttribute('style');
let matches;
if (color === null) {
matches = style ? style.match(/stop-color:\s?([#a-fA-F0-9]*)/) : false;
color = matches && matches.length > 1 ? matches[1] : undefined;
}
if (opacity === null) {
matches = style ? style.match(/stop-opacity:\s?([0-9.-]*)/) : false;
opacity = matches && matches.length > 1 ? parseFloat(matches[1]) : 1;
} else {
opacity = parseFloat(opacity);
}
stops.push(new Stop(offset, color, opacity));
}
const gradient = new LinearGradient(x1, y1, x2, y2, stops);
gradient.spread = spread;
gradient.units = units;
applySvgAttributes.call(this, node, gradient, parentStyles);
return gradient;
},
radialgradient: function (node, parentStyles) {
let units = node.getAttribute('gradientUnits');
let spread = node.getAttribute('spreadMethod');
if (!units) {
units = 'objectBoundingBox';
}
if (!spread) {
spread = 'pad';
}
let cx = parseFloat(node.getAttribute('cx')) || 0;
let cy = parseFloat(node.getAttribute('cy')) || 0;
let r = parseFloat(node.getAttribute('r'));
let fx = parseFloat(node.getAttribute('fx'));
let fy = parseFloat(node.getAttribute('fy'));
if (_.isNaN(fx)) {
fx = cx;
}
if (_.isNaN(fy)) {
fy = cy;
}
const ox = Math.abs(cx + fx) / 2;
const oy = Math.abs(cy + fy) / 2;
if (/userSpaceOnUse/i.test(units)) {
cx -= ox;
cy -= oy;
fx -= ox;
fy -= oy;
}
const stops = [];
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
let offset = child.getAttribute('offset');
if (/%/gi.test(offset)) {
offset = parseFloat(offset.replace(/%/gi, '')) / 100;
}
offset = parseFloat(offset);
let color = child.getAttribute('stop-color');
let opacity = child.getAttribute('stop-opacity');
let style = child.getAttribute('style');
let matches;
if (color === null) {
matches = style ? style.match(/stop-color:\s?([#a-fA-F0-9]*)/) : false;
color = matches && matches.length > 1 ? matches[1] : undefined;
}
if (opacity === null) {
matches = style ? style.match(/stop-opacity:\s?([0-9.-]*)/) : false;
opacity = matches && matches.length > 1 ? parseFloat(matches[1]) : 1;
} else {
opacity = parseFloat(opacity);
}
stops.push(new Stop(offset, color, opacity));
}
const gradient = new RadialGradient(cx, cy, r, stops, fx, fy);
gradient.spread = spread;
gradient.units = units;
applySvgAttributes.call(this, node, gradient, parentStyles);
return gradient;
},
text: function (node, parentStyles) {
const alignment = getAlignment(node.getAttribute('text-anchor')) || 'left';
const baseline = getBaseline(node) || 'baseline';
let message = '';
// Detect tspan for getting text content.
// If not, svg indentation apears in text content
if (node.childNodes.length > 0 && node.childNodes[0].tagName === 'TSPAN') {
message = node.childNodes[0].textContent;
} else {
message = node.textContent;
}
const text = new Text(message);
applySvgAttributes.call(this, node, text, parentStyles);
text.alignment = alignment;
text.baseline = baseline;
return text;
},
clippath: function (node, parentStyles) {
if (read.defs.current && !read.defs.current.contains(node.id)) {
read.defs.current.add(node.id, node);
}
return null;
},
image: function (node, parentStyles) {
let error;
const href = node.getAttribute('href') || node.getAttribute('xlink:href');
if (!href) {
error = new TwoError('encountered with no href.');
console.warn(error.name, error.message);
return null;
}
const x = parseFloat(node.getAttribute('x')) || 0;
const y = parseFloat(node.getAttribute('y')) || 0;
const width = parseFloat(node.getAttribute('width'));
const height = parseFloat(node.getAttribute('height'));
const sprite = new Sprite(href, x, y);
if (!_.isNaN(width)) {
sprite.width = width;
}
if (!_.isNaN(height)) {
sprite.height = height;
}
applySvgAttributes.call(this, node, sprite, parentStyles);
return sprite;
},
};
================================================
FILE: src/utils/math.d.ts
================================================
declare module 'two.js/src/utils/math' {
export interface DecomposedMatrix {
translateX: number;
translateY: number;
scaleX: number;
scaleY: number;
skewX: number;
skewY: number;
rotation: number;
}
/**
* @name Two.Utils.decomposeMatrix
* @function
* @param {Matrix} matrix - The matrix to decompose.
* @returns {Object} An object containing relevant skew values.
* @description Decompose a 2D 3x3 Matrix to find the skew.
*/
export function decomposeMatrix(matrix: Matrix): DecomposedMatrix;
export function decomposeMatrix(
a: number,
b: number,
c: number,
d: number,
e: number,
f: number
): DecomposedMatrix;
/**
* @name Two.Utils.getComputedMatrix
* @function
* @param {Shape} object - The Two.js object that has a matrix property to calculate from.
* @param {Matrix} [matrix] - The matrix to apply calculated transformations to if available.
* @returns {Matrix} The computed matrix of a nested object. If no `matrix` was passed in arguments then a `new Two.Matrix` is returned.
* @description Method to get the world space transformation of a given object in a Two.js scene.
*/
export function getComputedMatrix(object: Shape, matrix?: Matrix): Matrix;
export function getPoT(value: number | string): number;
export function setMatrix(matrix: Matrix): void;
/**
* @name Two.Utils.lerp
* @function
* @param {Number} a - Start value.
* @param {Number} b - End value.
* @param {Number} t - Zero-to-one value describing percentage between a and b.
* @returns {Number}
* @description Linear interpolation between two values `a` and `b` by an amount `t`.
*/
export function lerp(a: number, b: number, t: number): number;
/**
* @name Two.Utils.mod
* @function
* @param {Number} v - The value to modulo
* @param {Number} l - The value to modulo by
* @returns {Number}
* @description Modulo with added functionality to handle negative values in a positive manner.
*/
export function mod(v: number, l: number): number;
export const NumArray: Float32Array | number[];
/**
* @name Two.Utils.toFixed
* @function
* @param {Number} v - Any float
* @returns {Number} That float trimmed to the third decimal place.
* @description A pretty fast toFixed(3) alternative.
* @see {@link http://jsperf.com/parsefloat-tofixed-vs-math-round/18}
*/
export function toFixed(v: number): number;
export const TWO_PI: number;
export const HALF_PI: number;
/**
* @name Two.Utils.getEffectiveStrokeWidth
* @function
* @param {Path|Group} object - The object to calculate effective stroke width for
* @param {Matrix} [worldMatrix] - The world transformation matrix. If not provided, will be calculated.
* @returns {Number} The effective stroke width adjusted for strokeAttenuation setting
* @description Calculate effective stroke width, compensating for world scale if strokeAttenuation is false
*/
export function getEffectiveStrokeWidth(
object: Path | Group,
worldMatrix?: Matrix
): number;
import { Matrix } from 'two.js/src/matrix';
import { Shape, ShapeHitTestOptions } from 'two.js/src/shape';
import { Path } from 'two.js/src/path';
import { Group } from 'two.js/src/group';
}
================================================
FILE: src/utils/math.js
================================================
import { root } from './root.js';
let Matrix;
const TWO_PI = Math.PI * 2;
const HALF_PI = Math.PI * 0.5;
/**
* @name Two.Utils.decomposeMatrix
* @function
* @param {Two.Matrix} matrix - The matrix to decompose.
* @returns {Object} An object containing relevant skew values.
* @description Decompose a 2D 3x3 Matrix to find the skew.
*/
function decomposeMatrix(matrix, b, c, d, e, f) {
// TODO: Include skewX, skewY
// https://math.stackexchange.com/questions/237369/given-this-transformation-matrix-how-do-i-decompose-it-into-translation-rotati/417813
// https://stackoverflow.com/questions/45159314/decompose-2d-transformation-matrix
let a;
if (arguments.length <= 1) {
a = matrix.a;
b = matrix.b;
c = matrix.c;
d = matrix.d;
e = matrix.e;
f = matrix.f;
} else {
a = matrix;
}
return {
translateX: e,
translateY: f,
scaleX: Math.sqrt(a * a + b * b),
scaleY: Math.sqrt(c * c + d * d),
rotation: 180 * Math.atan2(b, a) / Math.PI
};
}
function setMatrix(matrix) {
Matrix = matrix;
}
/**
* @name Two.Utils.getComputedMatrix
* @function
* @param {Two.Shape} object - The Two.js object that has a matrix property to calculate from.
* @param {Two.Matrix} [matrix] - The matrix to apply calculated transformations to if available.
* @returns {Two.Matrix} The computed matrix of a nested object. If no `matrix` was passed in arguments then a `new Two.Matrix` is returned.
* @description Method to get the world space transformation of a given object in a Two.js scene.
*/
function getComputedMatrix(object, matrix) {
matrix = (matrix && matrix.identity()) || new Matrix();
let parent = object;
const matrices = [];
while (parent && parent._matrix) {
matrices.push(parent._matrix);
parent = parent.parent;
}
matrices.reverse();
for (let i = 0; i < matrices.length; i++) {
const m = matrices[i];
const e = m.elements;
matrix.multiply(
e[0], e[1], e[2], e[3], e[4], e[5], e[6], e[7], e[8]);
}
return matrix;
}
/**
* @name Two.Utils.lerp
* @function
* @param {Number} a - Start value.
* @param {Number} b - End value.
* @param {Number} t - Zero-to-one value describing percentage between a and b.
* @returns {Number}
* @description Linear interpolation between two values `a` and `b` by an amount `t`.
*/
function lerp(a, b, t) {
return t * (b - a) + a;
}
/**
* @name Two.Utils.getPoT
* @param {Number} value - The number to find the nearest power-of-two value
* @returns {Number}
* @description Rounds a number up to the nearest power-of-two value.
* @see {@link https://en.wikipedia.org/wiki/Power_of_two}
*/
const pots = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096];
function getPoT(value) {
let i = 0;
while (pots[i] && pots[i] < value) {
i++;
}
return pots[i];
}
/**
* @name Two.Utils.mod
* @function
* @param {Number} v - The value to modulo
* @param {Number} l - The value to modulo by
* @returns {Number}
* @description Modulo with added functionality to handle negative values in a positive manner.
*/
function mod(v, l) {
while (v < 0) {
v += l;
}
return v % l;
}
const NumArray = root.Float32Array || Array;
const floor = Math.floor;
/**
* @name Two.Utils.toFixed
* @function
* @param {Number} v - Any float
* @returns {Number} That float trimmed to the third decimal place.
* @description A pretty fast toFixed(3) alternative.
* @see {@link http://jsperf.com/parsefloat-tofixed-vs-math-round/18}
*/
function toFixed(v) {
return floor(v * 1000000) / 1000000;
}
/**
* @name Two.Utils.getEffectiveStrokeWidth
* @function
* @param {Two.Path|Two.Group} object - The object to calculate effective stroke width for.
* @param {Two.Matrix} [worldMatrix] - The world transformation matrix. If not provided, will be calculated.
* @returns {Number} The effective stroke width. If `object.strokeAttenuation` is true, returns the original linewidth (scales with transforms). If false, returns the linewidth compensated for world scale to maintain constant screen-space width.
* @description Calculates the effective stroke width for an object. If `strokeAttenuation` is true, returns the original linewidth (which scales with transforms). If `strokeAttenuation` is false, compensates for world scale so the stroke width remains constant in screen space.
*/
function getEffectiveStrokeWidth(object, worldMatrix) {
const linewidth = object._linewidth;
// If strokeAttenuation is true (default), return original linewidth (scales with transforms)
if (object.strokeAttenuation) {
return linewidth;
}
// Calculate world matrix if not provided
if (!worldMatrix) {
worldMatrix = object.worldMatrix || getComputedMatrix(object);
}
// Decompose matrix to get scale
const decomposed = decomposeMatrix(
worldMatrix.elements[0],
worldMatrix.elements[3],
worldMatrix.elements[1],
worldMatrix.elements[4],
worldMatrix.elements[2],
worldMatrix.elements[5]
);
// Use the larger of the two scale factors to maintain uniform appearance
const scale = Math.max(Math.abs(decomposed.scaleX), Math.abs(decomposed.scaleY));
// Compensate for scale to maintain constant screen-space width
return scale > 0 ? linewidth / scale : linewidth;
}
export {
decomposeMatrix, getComputedMatrix, getPoT, setMatrix, lerp, mod, NumArray,
toFixed, getEffectiveStrokeWidth, TWO_PI, HALF_PI
};
================================================
FILE: src/utils/path-commands.d.ts
================================================
declare module 'two.js/src/utils/path-commands' {
export interface Commands {
move: 'M';
line: 'L';
curve: 'C';
arc: 'A';
close: 'Z';
}
}
================================================
FILE: src/utils/path-commands.js
================================================
export const Commands = {
move: 'M',
line: 'L',
curve: 'C',
arc: 'A',
close: 'Z'
};
================================================
FILE: src/utils/path.js
================================================
import { lerp, mod } from './math.js';
import { Commands } from './path-commands.js';
import { Vector } from '../vector.js';
import { Anchor } from '../anchor.js';
const EPSILON = Number.EPSILON;
function isRelativeAnchor(anchor) {
return !(typeof anchor.relative === 'boolean') || !!anchor.relative;
}
function setHandleComponent(anchor, side, dx, dy) {
const controls = anchor.controls;
if (!controls || !controls[side]) {
return;
}
if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
if (isRelativeAnchor(anchor)) {
controls[side].clear();
} else {
controls[side].set(anchor.x, anchor.y);
}
return;
}
if (isRelativeAnchor(anchor)) {
controls[side].set(dx, dy);
} else {
controls[side].set(anchor.x + dx, anchor.y + dy);
}
}
function clearHandleComponent(anchor, side) {
setHandleComponent(anchor, side, 0, 0);
}
function getHandleOffset(anchor, side) {
const controls = anchor.controls;
if (!controls || !controls[side]) {
return { x: 0, y: 0 };
}
if (isRelativeAnchor(anchor)) {
return { x: controls[side].x, y: controls[side].y };
}
return {
x: controls[side].x - anchor.x,
y: controls[side].y - anchor.y,
};
}
function hasNonZeroHandle(anchor, side) {
const offset = getHandleOffset(anchor, side);
return Math.abs(offset.x) > EPSILON || Math.abs(offset.y) > EPSILON;
}
function updateAnchorCommand(anchor) {
if (anchor.command === Commands.move || anchor.command === Commands.close) {
return;
}
anchor.command =
hasNonZeroHandle(anchor, 'left') || hasNonZeroHandle(anchor, 'right')
? Commands.curve
: Commands.line;
}
function inheritRelative(anchor, reference) {
if (typeof reference.relative === 'boolean') {
anchor.relative = reference.relative;
}
}
function isSegmentCurved(a, b) {
return (
hasNonZeroHandle(b, 'right') ||
hasNonZeroHandle(a, 'left') ||
hasNonZeroHandle(a, 'right') ||
hasNonZeroHandle(b, 'left') ||
a.command === Commands.curve ||
b.command === Commands.curve
);
}
function lerpPoint(a, b, t) {
return {
x: lerp(a.x, b.x, t),
y: lerp(a.y, b.y, t),
};
}
function getAbsoluteHandle(anchor, side) {
const controls = anchor.controls && anchor.controls[side];
if (!controls) {
return { x: anchor.x, y: anchor.y };
}
if (isRelativeAnchor(anchor)) {
return { x: anchor.x + controls.x, y: anchor.y + controls.y };
}
return { x: controls.x, y: controls.y };
}
function splitSubdivisionSegment(start, end, t) {
const right = start.controls && start.controls.right;
const left = end.controls && end.controls.left;
const p0 = { x: start.x, y: start.y };
const p1 = right ? getAbsoluteHandle(start, 'right') : { ...p0 };
const p3 = { x: end.x, y: end.y };
const p2 = left ? getAbsoluteHandle(end, 'left') : { ...p3 };
const q0 = lerpPoint(p0, p1, t);
const q1 = lerpPoint(p1, p2, t);
const q2 = lerpPoint(p2, p3, t);
const r0 = lerpPoint(q0, q1, t);
const r1 = lerpPoint(q1, q2, t);
const point = lerpPoint(r0, r1, t);
const anchor = new Anchor(point.x, point.y);
inheritRelative(anchor, start);
setHandleComponent(anchor, 'left', r0.x - point.x, r0.y - point.y);
setHandleComponent(anchor, 'right', r1.x - point.x, r1.y - point.y);
anchor.command = Commands.curve;
return {
anchor,
startOut: q0,
endIn: q2,
};
}
function applyGlobalSmooth(vertices, from, to, closed, loop, asymmetric) {
const length = vertices.length;
const amount = to - from + 1;
let n = amount - 1;
let padding = loop ? Math.min(amount, 4) : 1;
let paddingLeft = padding;
let paddingRight = padding;
if (!closed) {
paddingLeft = Math.min(1, from);
paddingRight = Math.min(1, length - to - 1);
}
n += paddingLeft + paddingRight;
if (n <= 1) {
return;
}
const knots = new Array(n + 1);
for (let i = 0, j = from - paddingLeft; i <= n; i += 1, j += 1) {
const index = mod(j, length);
knots[i] = vertices[index];
}
let x = knots[0].x + 2 * knots[1].x;
let y = knots[0].y + 2 * knots[1].y;
let f = 2;
const n1 = n - 1;
const rx = [x];
const ry = [y];
const rf = [f];
const px = new Array(n + 1);
const py = new Array(n + 1);
for (let i = 1; i < n; i += 1) {
const internal = i < n1;
const a = internal ? 1 : asymmetric ? 1 : 2;
const b = internal ? 4 : asymmetric ? 2 : 7;
const u = internal ? 4 : asymmetric ? 3 : 8;
const v = internal ? 2 : asymmetric ? 0 : 1;
const m = a / f;
f = rf[i] = b - m;
x = rx[i] = u * knots[i].x + v * knots[i + 1].x - m * x;
y = ry[i] = u * knots[i].y + v * knots[i + 1].y - m * y;
}
px[n1] = rx[n1] / rf[n1];
py[n1] = ry[n1] / rf[n1];
for (let i = n - 2; i >= 0; i -= 1) {
px[i] = (rx[i] - px[i + 1]) / rf[i];
py[i] = (ry[i] - py[i + 1]) / rf[i];
}
px[n] = (3 * knots[n].x - px[n1]) / 2;
py[n] = (3 * knots[n].y - py[n1]) / 2;
const max = n - paddingRight;
for (let i = paddingLeft, j = from; i <= max; i += 1, j += 1) {
const index = mod(j, length);
const anchor = vertices[index];
const hx = px[i] - anchor.x;
const hy = py[i] - anchor.y;
if (loop || i < max) {
setHandleComponent(anchor, 'right', hx, hy);
} else {
clearHandleComponent(anchor, 'right');
}
if (loop || i > paddingLeft) {
setHandleComponent(anchor, 'left', -hx, -hy);
} else {
clearHandleComponent(anchor, 'left');
}
updateAnchorCommand(anchor);
}
}
function applyCatmullRom(anchor, prev, next, factor, clampIn, clampOut) {
const p0 = prev || anchor;
const p1 = anchor;
const p2 = next || anchor;
const d1 = Vector.distanceBetween(p0, p1);
const d2 = Vector.distanceBetween(p1, p2);
const a = factor === undefined ? 0.5 : factor;
const d1a = Math.pow(d1, a);
const d2a = Math.pow(d2, a);
const d1_2a = d1a * d1a;
const d2_2a = d2a * d2a;
if (!clampIn && prev) {
const A = 2 * d2_2a + 3 * d2a * d1a + d1_2a;
const N = 3 * d2a * (d2a + d1a);
if (N !== 0) {
const hx = (d2_2a * p0.x + A * p1.x - d1_2a * p2.x) / N - p1.x;
const hy = (d2_2a * p0.y + A * p1.y - d1_2a * p2.y) / N - p1.y;
setHandleComponent(anchor, 'left', hx, hy);
} else {
clearHandleComponent(anchor, 'left');
}
} else {
clearHandleComponent(anchor, 'left');
}
if (!clampOut && next) {
const A = 2 * d1_2a + 3 * d1a * d2a + d2_2a;
const N = 3 * d1a * (d1a + d2a);
if (N !== 0) {
const hx = (d1_2a * p2.x + A * p1.x - d2_2a * p0.x) / N - p1.x;
const hy = (d1_2a * p2.y + A * p1.y - d2_2a * p0.y) / N - p1.y;
setHandleComponent(anchor, 'right', hx, hy);
} else {
clearHandleComponent(anchor, 'right');
}
} else {
clearHandleComponent(anchor, 'right');
}
updateAnchorCommand(anchor);
}
function applyGeometric(anchor, prev, next, factor, clampIn, clampOut) {
if (!(prev && next)) {
if (!prev) {
clearHandleComponent(anchor, 'left');
}
if (!next) {
clearHandleComponent(anchor, 'right');
}
updateAnchorCommand(anchor);
return;
}
const p0 = prev;
const p1 = anchor;
const p2 = next;
const d1 = Vector.distanceBetween(p0, p1);
const d2 = Vector.distanceBetween(p1, p2);
const total = d1 + d2;
const tension = factor === undefined ? 0.4 : factor;
const vector = { x: p0.x - p2.x, y: p0.y - p2.y };
if (!clampIn && total !== 0) {
const k = (tension * d1) / total;
setHandleComponent(anchor, 'left', vector.x * k, vector.y * k);
} else {
clearHandleComponent(anchor, 'left');
}
if (!clampOut && total !== 0) {
const k = (tension * d1) / total - tension;
setHandleComponent(anchor, 'right', vector.x * k, vector.y * k);
} else {
clearHandleComponent(anchor, 'right');
}
updateAnchorCommand(anchor);
}
function applyLocalSmooth(vertices, from, to, closed, loop, options) {
const type = options.type || 'catmull-rom';
const factor = options.factor;
const length = vertices.length;
for (let i = from; i <= to; i += 1) {
const index = mod(i, length);
const anchor = vertices[index];
if (anchor.command === Commands.move) {
clearHandleComponent(anchor, 'left');
clearHandleComponent(anchor, 'right');
continue;
}
const prevIndex = i === from && !loop ? null : i - 1;
const nextIndex = i === to && !loop ? null : i + 1;
const prev = prevIndex === null ? null : vertices[mod(prevIndex, length)];
const next = nextIndex === null ? null : vertices[mod(nextIndex, length)];
const clampIn = prevIndex === null;
const clampOut = nextIndex === null;
if (type === 'geometric') {
applyGeometric(anchor, prev, next, factor, clampIn, clampOut);
} else {
applyCatmullRom(anchor, prev, next, factor, clampIn, clampOut);
}
}
}
export {
isRelativeAnchor,
setHandleComponent,
clearHandleComponent,
hasNonZeroHandle,
updateAnchorCommand,
inheritRelative,
isSegmentCurved,
splitSubdivisionSegment,
applyGlobalSmooth,
applyLocalSmooth,
};
================================================
FILE: src/utils/root.d.ts
================================================
declare module 'two.js/src/utils/root' {
export let root: typeof globalThis;
}
================================================
FILE: src/utils/root.js
================================================
let root;
if (typeof window !== 'undefined') {
root = window;
} else if (typeof global !== 'undefined') {
root = global;
} else if (typeof self !== 'undefined') {
root = self;
}
export { root };
================================================
FILE: src/utils/shaders.d.ts
================================================
declare module 'two.js/src/utils/shaders' {
export interface shaders {
create(gl: any, source: any, type: any): any;
types: {
vertex: string;
fragment: string;
};
path: {
vertex: string;
fragment: string;
};
points: {
vertex: string;
fragment: string;
};
}
}
================================================
FILE: src/utils/shaders.js
================================================
import { TwoError } from './error.js';
export const shaders = {
create: function(gl, source, type) {
const shader = gl.createShader(gl[type]);
gl.shaderSource(shader, source);
gl.compileShader(shader);
const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
const error = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new TwoError('unable to compile shader ' + shader + ': ' + error);
}
return shader;
},
types: {
vertex: 'VERTEX_SHADER',
fragment: 'FRAGMENT_SHADER'
},
path : {
vertex:`
precision mediump float;
attribute vec2 a_position;
uniform mat3 u_matrix;
uniform vec2 u_resolution;
uniform vec4 u_rect;
varying vec2 v_textureCoords;
void main() {
vec2 rectCoords = (a_position * (u_rect.zw - u_rect.xy)) + u_rect.xy;
vec2 projected = (u_matrix * vec3(rectCoords, 1.0)).xy;
vec2 normal = projected / u_resolution;
vec2 clipspace = (normal * 2.0) - 1.0;
gl_Position = vec4(clipspace * vec2(1.0, -1.0), 0.0, 1.0);
v_textureCoords = a_position;
}
`,
fragment: `
precision mediump float;
uniform sampler2D u_image;
varying vec2 v_textureCoords;
void main() {
vec4 texel = texture2D(u_image, v_textureCoords);
if (texel.a == 0.0) {
discard;
}
gl_FragColor = texel;
}
`,
},
points: {
vertex: `
precision mediump float;
attribute vec2 a_position;
uniform float u_size;
uniform mat3 u_matrix;
uniform vec2 u_resolution;
varying vec2 v_textureCoords;
void main() {
vec2 projected = (u_matrix * vec3(a_position, 1.0)).xy;
vec2 normal = projected / u_resolution;
vec2 clipspace = (normal * 2.0) - 1.0;
gl_PointSize = u_size;
gl_Position = vec4(clipspace * vec2(1.0, -1.0), 0.0, 1.0);
v_textureCoords = a_position;
}
`,
fragment: `
precision mediump float;
uniform sampler2D u_image;
void main() {
vec4 texel = texture2D(u_image, gl_PointCoord);
if (texel.a == 0.0) {
discard;
}
gl_FragColor = texel;
}
`
}
};
================================================
FILE: src/utils/shape.d.ts
================================================
declare module 'two.js/src/utils/shape' {
/**
* @private
* @param {Path} path - The path to analyze against.
* @param {Number} t -
* @returns {Number}
* @description
*/
export function contains(path: Path, t: number): number;
/**
* @private
* @param {Path} path - The path to analyze against.
* @param {Number} target - The target length at which to find an anchor.
* @returns {Number}
* @description Return the id of an anchor based on a target length.
*/
export function getIdByLength(path: Path, target: number): number;
export function getCurveLength(a: any, b: any, limit: any): number;
export function getSubdivisions(
a: any,
b: any,
limit: any
): import('two.js/src/anchor').Anchor[];
import { Path } from 'two.js/src/path';
}
================================================
FILE: src/utils/shape.js
================================================
import { Texture } from '../effects/texture.js';
import { subdivide, getCurveLength as gcl } from './curves.js';
import { Gradient } from '../effects/gradient.js';
import { LinearGradient } from '../effects/linear-gradient.js';
import { RadialGradient } from '../effects/radial-gradient.js';
/**
* @private
* @param {Two.Path} path - The path to analyze against.
* @param {Number} t -
* @returns {Number}
* @description
*/
function contains(path, t) {
if (t === 0 || t === 1) {
return true;
}
const length = path._length;
const target = length * t;
let elapsed = 0;
for (let i = 0; i < path._lengths.length; i++) {
const dist = path._lengths[i];
if (elapsed >= target) {
return target - elapsed >= 0;
}
elapsed += dist;
}
return false;
}
/**
* @private
* @param {Two.Path} path - The path to analyze against.
* @param {Number} target - The target length at which to find an anchor.
* @returns {Number}
* @description Return the id of an anchor based on a target length.
*/
function getIdByLength(path, target) {
const total = path._length;
if (target <= 0) {
return 0;
} else if (target >= total) {
return path._lengths.length - 1;
}
for (let i = 0, sum = 0; i < path._lengths.length; i++) {
if (sum + path._lengths[i] >= target) {
target -= sum;
return Math.max(i - 1, 0) + target / path._lengths[i];
}
sum += path._lengths[i];
}
return -1;
}
function getCurveLength(a, b, limit) {
// TODO: DRYness
let x1, x2, x3, x4, y1, y2, y3, y4;
const right = b.controls && b.controls.right;
const left = a.controls && a.controls.left;
x1 = b.x;
y1 = b.y;
x2 = (right || b).x;
y2 = (right || b).y;
x3 = (left || a).x;
y3 = (left || a).y;
x4 = a.x;
y4 = a.y;
if (right && b._relative) {
x2 += b.x;
y2 += b.y;
}
if (left && a._relative) {
x3 += a.x;
y3 += a.y;
}
return gcl(x1, y1, x2, y2, x3, y3, x4, y4, limit);
}
function getSubdivisions(a, b, limit) {
// TODO: DRYness
let x1, x2, x3, x4, y1, y2, y3, y4;
const right = b.controls && b.controls.right;
const left = a.controls && a.controls.left;
x1 = b.x;
y1 = b.y;
x2 = (right || b).x;
y2 = (right || b).y;
x3 = (left || a).x;
y3 = (left || a).y;
x4 = a.x;
y4 = a.y;
if (right && b._relative) {
x2 += b.x;
y2 += b.y;
}
if (left && a._relative) {
x3 += a.x;
y3 += a.y;
}
return subdivide(x1, y1, x2, y2, x3, y3, x4, y4, limit);
}
function getEffectFromObject(obj) {
switch (obj.renderer.type) {
case 'texture':
return Texture.fromObject(obj);
case 'gradient':
return Gradient.fromObject(obj);
case 'linear-gradient':
return LinearGradient.fromObject(obj);
case 'radial-gradient':
return RadialGradient.fromObject(obj);
}
return obj;
}
export {
contains,
getEffectFromObject,
getIdByLength,
getCurveLength,
getSubdivisions,
};
================================================
FILE: src/utils/underscore.d.ts
================================================
declare module 'two.js/src/utils/underscore' {
export interface _ {
isNaN(obj: any): boolean;
isElement(obj: any): boolean;
isObject(obj: any): boolean;
extend(base: any, ...args: any[]): any;
defaults(base: any, ...args: any[]): any;
each(obj: any, iteratee: any, context: any): any;
performance: { now: () => number };
}
}
================================================
FILE: src/utils/underscore.js
================================================
import { root } from './root.js';
const slice = Array.prototype.slice;
function isArrayLike(collection) {
if (collection === null || collection === undefined) return false;
const length = collection.length;
// Arrays cannot hold more than 2^32 - 1 items
return typeof length == 'number' && length >= 0 && length < 4294967296;
}
export const _ = {
isNaN: function (obj) {
return typeof obj === 'number' && obj !== +obj;
},
isElement: function (obj) {
return !!(obj && obj.nodeType === 1);
},
isObject: function (obj) {
const type = typeof obj;
return type === 'function' || (type === 'object' && !!obj);
},
isFunction: function (obj) {
return typeof obj === 'function';
},
extend: function (base) {
const sources = slice.call(arguments, 1);
for (let i = 0; i < sources.length; i++) {
const obj = sources[i];
for (let k in obj) {
base[k] = obj[k];
}
}
return base;
},
defaults: function (base) {
const sources = slice.call(arguments, 1);
for (let i = 0; i < sources.length; i++) {
const obj = sources[i];
for (let k in obj) {
if (base[k] === void 0) {
base[k] = obj[k];
}
}
}
return base;
},
each: function (obj, iteratee, context) {
const ctx = context || this;
const keys = !isArrayLike(obj) && Object.keys(obj);
const length = (keys || obj).length;
for (let i = 0; i < length; i++) {
const k = keys ? keys[i] : i;
iteratee.call(ctx, obj[k], k, obj);
}
return obj;
},
/**
* @name Two.Utils.performance
* @property {Date} - A special `Date` like object to get the current millis of the session. Used internally to calculate time between frames.
* e.g: `Utils.performance.now() // milliseconds since epoch`
*/
performance:
root.performance && root.performance.now ? root.performance : Date,
};
================================================
FILE: src/utils/xhr.d.ts
================================================
declare module 'two.js/src/utils/xhr' {
/**
* @name Two.Utils.xhr
* @function
* @param {String} path
* @param {Function} callback
* @returns {XMLHttpRequest} The constructed and called XHR request.
* @description Canonical method to initiate `GET` requests in the browser. Mainly used by {@link Two#load} method.
*/
export function xhr(
path: string,
callback: (resp: XMLHttpRequestResponseType) => void
): XMLHttpRequest;
}
================================================
FILE: src/utils/xhr.js
================================================
/**
* @name Two.Utils.xhr
* @function
* @param {String} path
* @param {Function} callback
* @returns {XMLHttpRequest} The constructed and called XHR request.
* @description Canonical method to initiate `GET` requests in the browser. Mainly used by {@link Two#load} method.
*/
export function xhr(path, callback) {
const xhr = new XMLHttpRequest();
xhr.open('GET', path);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
callback(xhr.responseText);
}
};
xhr.send();
return xhr;
}
================================================
FILE: src/vector.d.ts
================================================
declare module 'two.js/src/vector' {
/**
* @name Two.Vector
* @class
* @param {Number} [x=0] - Any number to represent the horizontal x-component of the vector.
* @param {Number} [y=0] - Any number to represent the vertical y-component of the vector.
* @description A class to store x / y component vector data. In addition to storing data `Two.Vector` has suped up methods for commonplace mathematical operations.
*/
export class Vector extends Events {
/**
* @name Two.Vector.zero
* @readonly
* @property {Vector} - Handy reference to a vector with component values 0, 0 at all times.
*/
static readonly zero: Vector;
/**
* @name Two.Vector.add
* @function
* @param {Vector} v1
* @param {Vector} v2
* @returns {Vector}
* @description Add two vectors together.
*/
static add(v1: Vector, v2: Vector): Vector;
/**
* @name Two.Vector.sub
* @function
* @param {Vector} v1
* @param {Vector} v2
* @returns {Vector}
* @description Subtract two vectors: `v2` from `v1`.
*/
static sub(v1: Vector, v2: Vector): Vector;
/**
* @name Two.Vector.subtract
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
static subtract(v1: Vector, v2: Vector): Vector;
/**
* @name Two.Vector.ratioBetween
* @function
* @param {Vector} v1
* @param {Vector} v2
* @returns {Number} The ratio betwen two points `v1` and `v2`.
*/
static ratioBetween(v1: Vector, v2: Vector): number;
/**
* @name Two.Vector.angleBetween
* @function
* @param {Vector} v1
* @param {Vector} v2
* @returns {Number} The angle between points `v1` and `v2`.
*/
static angleBetween(v1: Vector, v2: Vector): number;
static angleBetween(x1: number, y1: number, x2: number, y2: number): number;
/**
* @name Two.Vector.distanceBetween
* @function
* @param {Vector} v1
* @param {Vector} v2
* @returns {Number} The distance between points `v1` and `v2`. Distance is always positive.
*/
static distanceBetween(v1: Vector, v2: Vector): number;
/**
* @name Two.Vector.distanceBetweenSquared
* @function
* @param {Vector} v1
* @param {Vector} v2
* @returns {Number} The squared distance between points `v1` and `v2`.
*/
static distanceBetweenSquared(v1: Vector, v2: Vector): number;
constructor(x?: number, y?: number);
/**
* @name Two.Vector#_x
* @private
*/
private _x;
/**
* @name Two.Vector#_y
* @private
*/
private _y;
/**
* @name Two.Vector#x
* @property {Number} - The horizontal x-component of the vector.
* @type {Number}
*/
x: number;
/**
* @name Two.Vector#y
* @property {Number} - The vertical y-component of the vector.
* @type {Number}
*/
y: number;
set(x: number, y: number): Vector;
/**
* @name Two.Vector#copy
* @function
* @param {Vector} v
* @description Copy the x / y components of another object `v`.
*/
copy(v: Vector): Vector;
/**
* @name Two.Vector#clear
* @function
* @description Set the x / y component values of the vector to zero.
*/
clear(): Vector;
/**
* @name Two.Vector#clone
* @function
* @description Create a new vector and copy the existing values onto the newly created instance.
*/
clone(): Vector;
/**
* @name Two.Vector#add
* @function
* @param {Vector} v
* @description Add an object with x / y component values to the instance.
* @overloaded
*/
add(v: Vector): Vector;
/**
* @name Two.Vector#add
* @function
* @param {Number} v
* @description Add the **same** number to both x / y component values of the instance.
* @overloaded
*/
add(v: number): Vector;
/**
* @name Two.Vector#add
* @function
* @param {Number} x
* @param {Number} y
* @description Add `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
add(x: number, y: number): Vector;
/**
* @name Two.Vector#addSelf
* @function
* @description Alias for {@link Two.Vector.add}.
*/
addSelf(x: number, y: number): Vector;
addSelf(v: Vector): Vector;
addSelf(v: number): Vector;
/**
* @name Two.Vector#sub
* @function
* @param {Vector} v
* @description Subtract an object with x / y component values to the instance.
* @overloaded
*/
sub(v: Vector): Vector;
/**
* @name Two.Vector#sub
* @function
* @param {Number} v
* @description Subtract the **same** number to both x / y component values of the instance.
* @overloaded
*/
sub(v: number): Vector;
/**
* @name Two.Vector#sub
* @function
* @param {Number} x
* @param {Number} y
* @description Subtract `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
sub(x: number, y: number): Vector;
/**
* @name Two.Vector#subtract
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
subtract(x: number, y: number): Vector;
subtract(v: number): Vector;
subtract(v: Vector): Vector;
/**
* @name Two.Vector#subSelf
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
subSelf(x: number, y: number): Vector;
subSelf(v: number): Vector;
subSelf(v: Vector): Vector;
/**
* @name Two.Vector#subtractSelf
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
subtractSelft(x: number, y: number): Vector;
subtractSelft(v: number): Vector;
subtractSelft(v: Vector): Vector;
/**
* @name Two.Vector#multiply
* @function
* @param {Vector} v
* @description Multiply an object with x / y component values to the instance.
* @overloaded
*/
multiply(v: number): Vector;
/**
* @name Two.Vector#multiply
* @function
* @param {Number} v
* @description Multiply the **same** number to both x / y component values of the instance.
* @overloaded
*/
multiply(v: Vector): Vector;
/**
* @name Two.Vector#multiply
* @function
* @param {Number} x
* @param {Number} y
* @description Multiply `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
multiply(x: number, y: number): Vector;
/**
* @name Two.Vector#multiplySelf
* @function
* @description Alias for {@link Two.Vector.multiply}.
*/
multiplySelf(v: Vector, ...args: Vector[]): Vector;
/**
* @name Two.Vector#multiplyScalar
* @function
* @param {Number} s - The scalar to multiply by.
* @description Mulitiply the vector by a single number. Shorthand to call {@link Two.Vector#multiply} directly.
*/
multiplyScalar(s: number): Vector;
/**
* @name Two.Vector#divide
* @function
* @param {Vector} v
* @description Divide an object with x / y component values to the instance.
* @overloaded
*/
divide(v: Vector): Vector;
/**
* @name Two.Vector#divide
* @function
* @param {Number} v
* @description Divide the **same** number to both x / y component values of the instance.
* @overloaded
*/
divide(v: number): Vector;
/**
* @name Two.Vector#divide
* @function
* @param {Number} x
* @param {Number} y
* @description Divide `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
divide(x: number, y: number): Vector;
/**
* @name Two.Vector#divideSelf
* @function
* @description Alias for {@link Two.Vector.divide}.
*/
divideSelf(x: number, y: number): Vector;
divideSelf(v: number): Vector;
divideSelf(v: Vector): Vector;
/**
* @name Two.Vector#divideScalar
* @function
* @param {Number} s - The scalar to divide by.
* @description Divide the vector by a single number. Shorthand to call {@link Two.Vector#divide} directly.
*/
divideScalar(s: number): Vector;
/**
* @name Two.Vector#negate
* @function
* @description Invert each component's sign value.
*/
negate(): Vector;
/**
* @name Two.Vector#negate
* @function
* @returns {Number}
* @description Get the [dot product](https://en.wikipedia.org/wiki/Dot_product) of the vector.
*/
dot(v: Vector): number;
/**
* @name Two.Vector#length
* @function
* @returns {Number}
* @description Get the length of a vector.
*/
length(): number;
/**
* @name Two.Vector#lengthSquared
* @function
* @returns {Number}
* @description Get the length of the vector to the power of two. Widely used as less expensive than {@link Two.Vector#length} because it isn't square-rooting any numbers.
*/
lengthSquared(): number;
/**
* @name Two.Vector#normalize
* @function
* @description Normalize the vector from negative one to one.
*/
normalize(): Vector;
/**
* @name Two.Vector#distanceTo
* @function
* @returns {Number}
* @description Get the distance between two vectors.
*/
distanceTo(v: Vector): number;
/**
* @name Two.Vector#distanceToSquared
* @function
* @returns {Number}
* @description Get the distance between two vectors to the power of two. Widely used as less expensive than {@link Two.Vector#distanceTo} because it isn't square-rooting any numbers.
*/
distanceToSquared(v: Vector): number;
/**
* @name Two.Vector#setLength
* @function
* @param {Number} l - length to set vector to.
* @description Set the length of a vector.
*/
setLength(l: number): Vector;
/**
* @name Two.Vector#equals
* @function
* @param {Vector} v - The vector to compare against.
* @param {Number} [eps=0.0001] - An options epsilon for precision.
* @returns {Boolean}
* @description Qualify if one vector roughly equal another. With a margin of error defined by epsilon.
*/
equals(v: Vector, eps?: number): boolean;
/**
* @name Two.Vector#lerp
* @function
* @param {Vector} v - The destination vector to step towards.
* @param {Number} t - The zero to one value of how close the current vector gets to the destination vector.
* @description Linear interpolate one vector to another by an amount `t` defined as a zero to one number.
* @see [Matt DesLauriers](https://twitter.com/mattdesl/status/1031305279227478016) has a good thread about this.
*/
lerp(v: Vector, t: number): Vector;
/**
* @name Two.Vector#isZero
* @function
* @param {Number} [eps=0.0001] - Optional precision amount to check against.
* @returns {Boolean}
* @description Check to see if vector is roughly zero, based on the `epsilon` precision value.
*/
isZero(eps?: number): boolean;
/**
* @name Two.Vector#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the vector.
*/
toObject(): object;
/**
* @name Two.Vector#rotate
* @function
* @param {Number} radians - The amount to rotate the vector by in radians.
* @description Rotate a vector.
*/
rotate(radians: number): Vector;
}
import { Events } from 'two.js/src/events';
}
================================================
FILE: src/vector.js
================================================
import { Events } from './events.js';
import { toFixed } from './utils/math.js';
const proto = {
x: {
enumerable: true,
get: function () {
return this._x;
},
set: function (v) {
if (this._x !== v) {
this._x = v;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
},
},
y: {
enumerable: true,
get: function () {
return this._y;
},
set: function (v) {
if (this._y !== v) {
this._y = v;
if (this._bound) {
this.dispatchEvent(Events.Types.change);
}
}
},
},
};
/**
* @name Two.Vector
* @class
* @extends Two.Events
* @param {Number} [x=0] - Any number to represent the horizontal `x` component of the vector.
* @param {Number} [y=0] - Any number to represent the vertical `y` component of the vector.
* @description A class to store `x` / `y` component vector data. In addition to storing data `Two.Vector` has suped up methods for commonplace mathematical operations.
*/
export class Vector extends Events {
/**
* @name Two.Vector#_x
* @private
*/
_x = 0;
/**
* @name Two.Vector#_y
* @private
*/
_y = 0;
constructor(x = 0, y = 0) {
super();
for (let prop in proto) {
Object.defineProperty(this, prop, proto[prop]);
}
/**
* @name Two.Vector#x
* @property {Number} - The horizontal x-component of the vector.
* @type {Number}
*/
this.x = x;
/**
* @name Two.Vector#y
* @property {Number} - The vertical y-component of the vector.
* @type {Number}
*/
this.y = y;
}
/**
* @name Two.Vector.zero
* @readonly
* @property {Two.Vector} - Handy reference to a vector with component values 0, 0 at all times.
*/
static zero = new Vector();
/**
* @name Two.Vector.left
* @readonly
* @property {Two.Vector} - Handy reference to a vector with component values -1, 0 at all times.
*/
static left = new Vector(-1, 0);
/**
* @name Two.Vector.right
* @readonly
* @property {Two.Vector} - Handy reference to a vector with component values 1, 0 at all times.
*/
static right = new Vector(1, 0);
/**
* @name Two.Vector.up
* @readonly
* @property {Two.Vector} - Handy reference to a vector with component values 0, -1 at all times.
*/
static up = new Vector(0, -1);
/**
* @name Two.Vector.down
* @readonly
* @property {Two.Vector} - Handy reference to a vector with component values 0, 1 at all times.
*/
static down = new Vector(0, 1);
/**
* @name Two.Vector.add
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Two.Vector}
* @description Add two vectors together.
*/
static add(v1, v2) {
return new Vector(v1.x + v2.x, v1.y + v2.y);
}
/**
* @name Two.Vector.sub
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Two.Vector}
* @description Subtract two vectors: `v2` from `v1`.
*/
static sub(v1, v2) {
return new Vector(v1.x - v2.x, v1.y - v2.y);
}
/**
* @name Two.Vector.subtract
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
static subtract(v1, v2) {
return Vector.sub(v1, v2);
}
/**
* @name Two.Vector.ratioBetween
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Number} The ratio betwen two points `v1` and `v2`.
*/
static ratioBetween(v1, v2) {
return (v1.x * v2.x + v1.y * v2.y) / (v1.length() * v2.length());
}
/**
* @name Two.Vector.angleBetween
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Number} The angle between points `v1` and `v2`.
*/
static angleBetween(v1, v2) {
if (arguments.length >= 4) {
const dx = arguments[0] - arguments[2];
const dy = arguments[1] - arguments[3];
return Math.atan2(dy, dx);
}
const dx = v1.x - v2.x;
const dy = v1.y - v2.y;
return Math.atan2(dy, dx);
}
/**
* @name Two.Vector.distanceBetween
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Number} The distance between points `v1` and `v2`. Distance is always positive.
*/
static distanceBetween(v1, v2) {
return Math.sqrt(Vector.distanceBetweenSquared(v1, v2));
}
/**
* @name Two.Vector.distanceBetweenSquared
* @function
* @param {Two.Vector} v1 - First {@link Two.Vector}
* @param {Two.Vector} v2 - Second {@link Two.Vector}
* @returns {Number} The squared distance between points `v1` and `v2`.
*/
static distanceBetweenSquared(v1, v2) {
const dx = v1.x - v2.x;
const dy = v1.y - v2.y;
return dx * dx + dy * dy;
}
//
/**
* @name Two.Vector#set
* @function
* @param {number} x - Value of `x` component
* @param {number} y - Value of `y` component
*/
set(x, y) {
this.x = x;
this.y = y;
return this;
}
/**
* @name Two.Vector#copy
* @function
* @param {Two.Vector} v - The {@link Two.Vector} to copy
* @description Copy the `x` / `y` components of another object {@link Two.Vector}.
*/
copy(v) {
this.x = v.x;
this.y = v.y;
return this;
}
/**
* @name Two.Vector#clear
* @function
* @description Set the `x` / `y` component values of the vector to zero.
*/
clear() {
this.x = 0;
this.y = 0;
return this;
}
/**
* @name Two.Vector#clone
* @function
* @description Create a new vector and copy the existing values onto the newly created instance.
* @return {Two.Vector}
*/
clone() {
return new Vector(this.x, this.y);
}
/**
* @name Two.Vector#add
* @function
* @param {Two.Vector} v - The {@link Two.Vector} to add
* @description Add an object with `x` / `y` component values to the instance.
* @overloaded
*/
/**
* @name Two.Vector#add
* @function
* @param {Number} n - Number to add
* @description Add the **same** number to both `x` / `y` component values of the instance.
* @overloaded
*/
/**
* @name Two.Vector#add
* @function
* @param {Number} x - Number to add to `x` component
* @param {Number} y - Number to add to `y` component
* @description Add `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
add(x, y) {
if (arguments.length <= 0) {
return this;
} else if (arguments.length <= 1) {
if (typeof x === 'number') {
this.x += x;
this.y += x;
} else if (x && typeof x.x === 'number' && typeof x.y === 'number') {
this.x += x.x;
this.y += x.y;
}
} else {
this.x += x;
this.y += y;
}
return this;
}
/**
* @name Two.Vector#addSelf
* @function
* @description Alias for {@link Two.Vector.add}.
*/
addSelf(v) {
return this.add.apply(this, arguments);
}
/**
* @name Two.Vector#sub
* @function
* @param {Two.Vector} v - The amount as a {@link Two.Vector} to subtract
* @description Subtract an object with `x` / `y` component values to the instance.
* @overloaded
*/
/**
* @name Two.Vector#sub
* @function
* @param {Number} n - Number to subtract
* @description Subtract the **same** number to both `x` / `y` component values of the instance.
* @overloaded
*/
/**
* @name Two.Vector#sub
* @function
* @param {Number} x - Number to subtract from `x` component
* @param {Number} y - Number to subtract from `y` component
* @description Subtract `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
sub(x, y) {
if (arguments.length <= 0) {
return this;
} else if (arguments.length <= 1) {
if (typeof x === 'number') {
this.x -= x;
this.y -= x;
} else if (x && typeof x.x === 'number' && typeof x.y === 'number') {
this.x -= x.x;
this.y -= x.y;
}
} else {
this.x -= x;
this.y -= y;
}
return this;
}
/**
* @name Two.Vector#subtract
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
subtract() {
return this.sub.apply(this, arguments);
}
/**
* @name Two.Vector#subSelf
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
subSelf(v) {
return this.sub.apply(this, arguments);
}
/**
* @name Two.Vector#subtractSelf
* @function
* @description Alias for {@link Two.Vector.sub}.
*/
subtractSelf(v) {
return this.sub.apply(this, arguments);
}
/**
* @name Two.Vector#multiply
* @function
* @param {Two.Vector} v - The {@link Two.Vector} to multiply
* @description Multiply an object with `x` / `y` component values to the instance.
* @overloaded
*/
/**
* @name Two.Vector#multiply
* @function
* @param {Number} n - The number to multiply
* @description Multiply the **same** number to both x / y component values of the instance.
* @overloaded
*/
/**
* @name Two.Vector#multiply
* @function
* @param {Number} x - The number to multiply to `x` component
* @param {Number} y - The number to multiply to `y` component
* @description Multiply `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
multiply(x, y) {
if (arguments.length <= 0) {
return this;
} else if (arguments.length <= 1) {
if (typeof x === 'number') {
this.x *= x;
this.y *= x;
} else if (x && typeof x.x === 'number' && typeof x.y === 'number') {
this.x *= x.x;
this.y *= x.y;
}
} else {
this.x *= x;
this.y *= y;
}
return this;
}
/**
* @name Two.Vector#multiplySelf
* @function
* @description Alias for {@link Two.Vector.multiply}.
*/
multiplySelf(v) {
return this.multiply.apply(this, arguments);
}
/**
* @name Two.Vector#multiplyScalar
* @function
* @param {Number} s - The scalar to multiply by.
* @description Mulitiply the vector by a single number. Shorthand to call {@link Two.Vector#multiply} directly.
*/
multiplyScalar(s) {
return this.multiply(s);
}
/**
* @name Two.Vector#divide
* @function
* @param {Two.Vector} v - The {@link Two.Vector} to divide
* @description Divide an object with `x` / `y` component values to the instance.
* @overloaded
*/
/**
* @name Two.Vector#divide
* @function
* @param {Number} n - The number to divide
* @description Divide the **same** number to both x / y component values of the instance.
* @overloaded
*/
/**
* @name Two.Vector#divide
* @function
* @param {Number} x - The number to divide on the `x` component
* @param {Number} y - The number to divide on the `y` component
* @description Divide `x` / `y` values to their respective component value on the instance.
* @overloaded
*/
divide(x, y) {
if (arguments.length <= 0) {
return this;
} else if (arguments.length <= 1) {
if (typeof x === 'number') {
this.x /= x;
this.y /= x;
} else if (x && typeof x.x === 'number' && typeof x.y === 'number') {
this.x /= x.x;
this.y /= x.y;
}
} else {
this.x /= x;
this.y /= y;
}
if (isNaN(this.x)) {
this.x = 0;
}
if (isNaN(this.y)) {
this.y = 0;
}
return this;
}
/**
* @name Two.Vector#divideSelf
* @function
* @description Alias for {@link Two.Vector.divide}.
*/
divideSelf(v) {
return this.divide.apply(this, arguments);
}
/**
* @name Two.Vector#divideScalar
* @function
* @param {Number} s - The scalar to divide by.
* @description Divide the vector by a single number. Shorthand to call {@link Two.Vector#divide} directly.
*/
divideScalar(s) {
return this.divide(s);
}
/**
* @name Two.Vector#negate
* @function
* @description Invert each component's sign value.
*/
negate() {
return this.multiply(-1);
}
/**
* @name Two.Vector#dot
* @function
* @returns {Number}
* @description Get the [dot product](https://en.wikipedia.org/wiki/Dot_product) of the vector.
*/
dot(v) {
return this.x * v.x + this.y * v.y;
}
/**
* @name Two.Vector#length
* @function
* @returns {Number}
* @description Get the length of a vector.
*/
length() {
return Math.sqrt(this.lengthSquared());
}
/**
* @name Two.Vector#lengthSquared
* @function
* @returns {Number}
* @description Get the length of the vector to the power of two. Widely used as less expensive than {@link Two.Vector#length} because it isn't square-rooting any numbers.
*/
lengthSquared() {
return this.x * this.x + this.y * this.y;
}
/**
* @name Two.Vector#normalize
* @function
* @description Normalize the vector from negative one to one.
*/
normalize() {
return this.divideScalar(this.length());
}
/**
* @name Two.Vector#distanceTo
* @function
* @returns {Number}
* @description Get the distance between two vectors.
*/
distanceTo(v) {
return Math.sqrt(this.distanceToSquared(v));
}
/**
* @name Two.Vector#distanceToSquared
* @function
* @returns {Number}
* @description Get the distance between two vectors to the power of two. Widely used as less expensive than {@link Two.Vector#distanceTo} because it isn't square-rooting any numbers.
*/
distanceToSquared(v) {
const dx = this.x - v.x;
const dy = this.y - v.y;
return dx * dx + dy * dy;
}
/**
* @name Two.Vector#setLength
* @function
* @param {Number} l - length to set vector to.
* @description Set the length of a vector.
*/
setLength(l) {
return this.normalize().multiplyScalar(l);
}
/**
* @name Two.Vector#equals
* @function
* @param {Two.Vector} v - The vector to compare against.
* @param {Number} [eps=0.0001] - An options epsilon for precision.
* @returns {Boolean}
* @description Qualify if one vector roughly equal another. With a margin of error defined by epsilon.
*/
equals(v, eps) {
eps = typeof eps === 'undefined' ? 0.0001 : eps;
return this.distanceTo(v) < eps;
}
/**
* @name Two.Vector#lerp
* @function
* @param {Two.Vector} v - The destination vector to step towards.
* @param {Number} t - The zero to one value of how close the current vector gets to the destination vector.
* @description Linear interpolate one vector to another by an amount `t` defined as a zero to one number.
* @see [Matt DesLauriers](https://twitter.com/mattdesl/status/1031305279227478016) has a good thread about this.
*/
lerp(v, t) {
const x = (v.x - this.x) * t + this.x;
const y = (v.y - this.y) * t + this.y;
return this.set(x, y);
}
/**
* @name Two.Vector#isZero
* @function
* @param {Number} [eps=0.0001] - Optional precision amount to check against.
* @returns {Boolean}
* @description Check to see if vector is roughly zero, based on the `epsilon` precision value.
*/
isZero(eps) {
eps = typeof eps === 'undefined' ? 0.0001 : eps;
return this.length() < eps;
}
/**
* @name Two.Vector#toString
* @function
* @returns {String}
* @description Return a comma-separated string of x, y value. Great for storing in a database.
*/
toString() {
return this.x + ', ' + this.y;
}
/**
* @name Two.Vector#toObject
* @function
* @returns {Object}
* @description Return a JSON compatible plain object that represents the vector.
*/
toObject() {
return { x: toFixed(this.x), y: toFixed(this.y) };
}
/**
* @name Two.Vector#rotate
* @function
* @param {Number} radians - The amount to rotate the vector by in radians.
* @description Rotate a vector.
*/
rotate(radians) {
const x = this.x;
const y = this.y;
const cos = Math.cos(radians);
const sin = Math.sin(radians);
this.x = x * cos - y * sin;
this.y = x * sin + y * cos;
return this;
}
}
================================================
FILE: tests/index.html
================================================
Two.js Tests
================================================
FILE: tests/noWebGL.html
================================================
Two.js Tests sans WebGL
================================================
FILE: tests/src/utils.js
================================================
/**
* Convenience properties and methods for QUnit testing within two.js
*/
(function () {
var root = this;
var QU = root.QUnit || {};
var TEMP = document.createElement('div');
var Tolerance = 0.001;
var Utils = (QU.Utils = {
digits: function (v, d) {
var r = '';
var s = v + '';
var diff = Math.max(d - s.length, 0);
var i = 0;
while (i < diff) {
r += '0';
i++;
}
return r + v;
},
getSelector: function (test) {
return '#qunit-test-output-' + test.testId + ' ol';
},
/**
* Add a DOM Element to your current unit test.
*/
addElemToTest: function (test, elem) {
// // Skip for headless
// if (window.URL) return;
var domElement = document.createElement('li');
if (Array.isArray(elem)) {
_.each(elem, function (el) {
domElement.appendChild(el);
});
} else {
domElement.appendChild(elem);
}
_.delay(function () {
var selector = Utils.getSelector(test);
document.querySelector(selector).appendChild(domElement);
}, 100);
},
/**
* Add an instance of Two.js to your current unit test.
*/
addInstanceToTest: function (test, two) {
var elem;
if (Array.isArray(two)) {
elem = two.map(function (t) {
var el = t.renderer.domElement;
switch (el.tagName.toLowerCase()) {
case 'svg':
break;
default:
el.style.width = 200 + 'px';
el.style.height = (200 * el.height) / el.width + 'px';
}
el.style.border = '1px solid #ccc';
return el;
});
} else {
elem = two.renderer.domElement;
switch (elem.tagName.toLowerCase()) {
case 'svg':
break;
default:
elem.style.width = 200 + 'px';
elem.style.height = (200 * elem.height) / elem.width + 'px';
}
elem.style.border = '1px solid #ccc';
}
Utils.addElemToTest(test, elem);
},
get: function (path, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', path, true);
xhr.onreadystatechange = function (e) {
if (xhr.readyState !== 4 || xhr.status !== 200) {
return;
}
callback(xhr.response);
};
xhr.send();
},
/**
* Ajax get request to get blob.
*/
getImageBlob: function (path, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', path, true);
if (window.URL) {
xhr.responseType = 'blob';
} else {
xhr.responseType = 'arraybuffer';
}
xhr.onreadystatechange = function (e) {
if (xhr.readyState !== 4 || xhr.status !== 200) {
return;
}
if (window.URL) {
callback(this.response);
} else {
var blob;
var mimeString = 'image/png';
// Some older Webkits don't support responseType blob,
// So create a blob from arraybuffer
try {
blob = new Blob([this.response], { type: mimeString });
} catch (e) {
// The BlobBuilder API has been deprecated in favour of Blob, but older
// browsers don't know about the Blob constructor
// IE10 also supports BlobBuilder, but since the `Blob` constructor
// also works, there's no need to add `MSBlobBuilder`.
var BlobBuilder = window.WebKitBlobBuilder || window.MozBlobBuilder;
var bb = new BlobBuilder();
bb.append(this.response);
blob = bb.getBlob(mimeString);
}
callback(blob);
}
};
xhr.send();
},
/**
* Compare a specific instance of two to an image in the context of a
* specific test.
*/
compare: function (path, renderer, message, callback) {
var assert = this;
QUnit.Utils.getImageBlob(path, function (reference) {
var data = renderer.domElement.toDataURL('image/png');
resemble(reference)
.compareTo(data)
.onComplete(function (data) {
var pct = parseFloat(data.misMatchPercentage);
// Can differ a bit due to antialiasing etc.
assert.ok(pct <= 3, message);
if (assert.done) {
assert.done();
}
var img = document.createElement('img');
img.src = path;
img.title = 'Reference Image';
img.width = 200;
img.style.border = '1px solid #ccc';
var domElement = document.createElement('li');
renderer.domElement.title = 'Computed Image';
renderer.domElement.style.border = '1px solid #000';
renderer.domElement.style.width = 200 + 'px';
renderer.domElement.style.height =
200 * (renderer.height / renderer.width) + 'px';
renderer.domElement.style.marginLeft = 10 + 'px';
domElement.appendChild(img);
domElement.appendChild(renderer.domElement);
_.delay(function () {
var selector = Utils.getSelector(assert.test);
document.querySelector(selector).appendChild(domElement);
}, 100);
if (typeof callback === 'function') {
callback();
}
});
});
},
textToDOM: function (str) {
TEMP.innerHTML = str;
return Array.prototype.map.call(TEMP.children, function (child) {
return child;
});
},
/**
* Deep equality between an answer, a, and an object in question, q.
*/
shapeEquals: function (a, q) {
for (var i in a) {
var check;
if (Array.isArray(a[i])) {
check = Utils.shapeEquals(a[i], Array.prototype.slice.call(q[i]));
} else if (_.isObject(a[i])) {
check = Utils.shapeEquals(a[i], q[i]);
} else if (typeof a[i] === 'number') {
check = Math.abs(a[i] - q[i]) <= Tolerance; // Fuzzy checking
} else {
check = q[i] === a[i];
}
if (!check) {
return false;
}
}
return true;
},
});
})();
================================================
FILE: tests/suite/bounding-box.js
================================================
/**
* Tests Two.js Utilities related to getBoundingClientRect methods:
* + polygon._matrix transformations
* + Two.getComputedMatrix
*/
(function () {
QUnit.module('getBoundingClientRect');
QUnit.test('Two.Path.getBoundingClientRect', function (assert) {
assert.expect(5);
// assert.done = assert.async(4);
(function () {
var two = new Two({
width: 400,
height: 400,
});
var answer = {
top: 134.671853,
left: 134.671853,
right: 265.328146,
bottom: 265.328146,
width: 130.656293,
height: 130.656293,
};
var shape = two.makeRectangle(200, 200, 100, 100);
shape.rotation = Math.PI / 8;
shape.noStroke().fill = 'rgb(60, 209, 201)';
var box = two.makeRectangle(
answer.left + answer.width / 2,
answer.top + answer.height / 2,
answer.width,
answer.height
);
box.noFill();
two.update();
var rect = shape.getBoundingClientRect();
for (let prop in rect) {
var value = rect[prop];
rect[prop] = Two.Utils.toFixed(value);
}
var a1 = JSON.stringify(answer);
var a2 = JSON.stringify(rect);
assert.equal(
a1,
a2,
true,
'Two.Path.getBoundingClientRect properly calculates rotated shapes.'
);
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement]);
})();
(function () {
var two = new Two({
width: 400,
height: 400,
});
var answer = {
top: 129.671853,
left: 129.671853,
right: 270.328146,
bottom: 270.328146,
width: 140.656293,
height: 140.656293,
};
var shape = two.makeRectangle(200, 200, 50, 50);
shape.rotation = Math.PI / 8;
shape.fill = 'rgb(60, 209, 201)';
shape.stroke = '#ccc';
shape.linewidth = 5;
shape.scale = 2;
var box = two.makeRectangle(
answer.left + answer.width / 2,
answer.top + answer.height / 2,
answer.width,
answer.height
);
box.noFill();
two.update();
var rect = shape.getBoundingClientRect();
for (let prop in rect) {
var value = rect[prop];
rect[prop] = Two.Utils.toFixed(value);
}
var a1 = JSON.stringify(answer);
var a2 = JSON.stringify(rect);
assert.equal(
a1,
a2,
true,
'Two.Path.getBoundingClientRect properly calculates scaled shapes.'
);
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement]);
})();
(function () {
var two = new Two({
width: 400,
height: 400,
});
var answer = {
top: 150,
left: 150,
right: 250,
bottom: 250,
width: 100,
height: 100,
};
var shape = two.makeCircle(200, 200, 50);
shape.fill = 'rgb(60, 209, 201)';
shape.linewidth = 0;
var box = two.makeRectangle(
answer.left + answer.width / 2,
answer.top + answer.height / 2,
answer.width,
answer.height
);
box.noFill();
two.update();
var rect = shape.getBoundingClientRect();
for (let prop in rect) {
var value = rect[prop];
rect[prop] = Two.Utils.toFixed(value);
}
var a1 = JSON.stringify(answer);
var a2 = JSON.stringify(rect);
assert.equal(
a1,
a2,
true,
'Two.Path.getBoundingClientRect properly calculates circles.'
);
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement]);
})();
(function () {
var two = new Two({
width: 400,
height: 400,
});
var answer = {
top: 150,
left: 150,
right: 249.999999,
bottom: 249.999999,
width: 99.999998,
height: 99.999998,
};
var shape = two.makeCircle(200, 200, 50);
shape.fill = 'rgb(60, 209, 201)';
shape.linewidth = 0;
shape.rotation = Math.PI / 4;
var box = two.makeRectangle(
answer.left + answer.width / 2,
answer.top + answer.height / 2,
answer.width,
answer.height
);
box.noFill();
two.update();
var rect = shape.getBoundingClientRect();
for (let prop in rect) {
var value = rect[prop];
rect[prop] = Two.Utils.toFixed(value);
}
var a1 = JSON.stringify(answer);
var a2 = JSON.stringify(rect);
assert.equal(
a1,
a2,
true,
'Two.Path.getBoundingClientRect properly calculates rotated circles (projected).'
);
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement]);
})();
(function () {
var two = new Two({
width: 400,
height: 400,
});
var answer = {
top: 182.752229,
left: 167.233141,
right: 232.766858,
bottom: 217.24777,
width: 65.533717,
height: 34.495541,
};
var shape = two.makeText('Hello World', 200, 200);
shape.rotation = Math.PI / 8;
shape.noStroke().fill = 'rgb(60, 209, 201)';
var box = two.makeRectangle(
answer.left + answer.width / 2,
answer.top + answer.height / 2,
answer.width,
answer.height
);
box.noFill();
two.update();
var rect = shape.getBoundingClientRect();
for (let prop in rect) {
var value = rect[prop];
rect[prop] = Two.Utils.toFixed(value);
}
var a1 = JSON.stringify(answer);
var a2 = JSON.stringify(rect);
assert.equal(
a1,
a2,
true,
'Two.Text.getBoundingClientRect properly calculates rotated shapes.'
);
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement]);
})();
});
QUnit.test('Two.getComputedMatrix', function (assert) {
assert.expect(2);
// assert.done = assert.async(1);
(function () {
var two = new Two({
width: 400,
height: 400,
});
two.renderer.domElement.style.border = '1px solid blue';
var parentGroup = two.makeGroup();
parentGroup.scale = 2;
parentGroup.translation.set(two.width / 2, two.height / 2);
parentGroup.rotation = Math.PI / 8;
var group = two.makeGroup();
group.scale = 2;
group.translation.set(50, 0);
group.rotation = Math.PI / 8;
var shape = two.makeRectangle(0, 0, 50, 50);
shape.fill = 'black';
shape.stroke = 'yellow';
shape.linewidth = 2;
parentGroup.add(group);
group.add(shape);
var answer = {
top: 95.846993,
left: 149.966591,
right: 434.809287,
bottom: 380.689688,
width: 284.842695,
height: 284.842695,
};
var bBox = group.getBoundingClientRect();
var rect = two.makeRectangle(
bBox.left + bBox.width / 2,
bBox.top + bBox.height / 2,
bBox.width,
bBox.height
);
rect.noFill().stroke = 'orangered';
var bBoxClose = shape.getBoundingClientRect();
var rectClose = two.makeRectangle(
bBoxClose.left + bBoxClose.width / 2,
bBoxClose.top + bBoxClose.height / 2,
bBoxClose.width,
bBoxClose.height
);
rectClose.noFill().stroke = 'green';
two.update();
for (var prop in bBox) {
bBox[prop] = Two.Utils.toFixed(bBox[prop]);
bBoxClose[prop] = Two.Utils.toFixed(bBoxClose[prop]);
}
var a1 = JSON.stringify(answer);
var a2 = JSON.stringify(bBox);
var a3 = JSON.stringify(bBoxClose);
assert.equal(
a1,
a2,
true,
'Two.Path.getBoundingClientRect properly calculates parent’s Two.Group.getBoundingClientRect.'
);
assert.equal(
a1,
a3,
true,
'Two.Path.getBoundingClientRect properly calculates child’s Two.Group.getBoundingClientRect.'
);
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement]);
})();
});
})();
================================================
FILE: tests/suite/canvas.js
================================================
/**
* Tests Two.js Canvas Rendering Functionality:
*/
(function () {
QUnit.module('CanvasRenderer');
var getRatio = function (v) {
return Math.round(window.devicePixelRatio);
};
var deviceRatio = getRatio(document.createElement('canvas').getContext('2d'));
var suffix = '@' + deviceRatio + 'x.png';
QUnit.test('Two.makeLine', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 400,
height: 400,
ratio: deviceRatio,
});
two.makeLine(0, 0, two.width, two.height);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/line' + suffix,
two.renderer,
'Two.makeLine renders properly.'
);
});
QUnit.test('Two.makeRectangle', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 400,
height: 400,
ratio: deviceRatio,
});
two.makeRectangle(two.width / 2, two.height / 2, 100, 100);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/rectangle' + suffix,
two.renderer,
'Two.makeRectangle renders properly.'
);
});
QUnit.test('Two.makeEllipse', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 400,
height: 400,
ratio: deviceRatio,
});
two.makeEllipse(two.width / 2, two.height / 2, 100, 100);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/ellipse' + suffix,
two.renderer,
'Two.makeEllipse renders properly.'
);
});
QUnit.test('Two.makeCircle', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 400,
height: 400,
ratio: deviceRatio,
});
two.makeCircle(two.width / 2, two.height / 2, 50);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/circle' + suffix,
two.renderer,
'Two.makeCircle renders properly.'
);
});
QUnit.test('Two.makePoints', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 400,
height: 400,
ratio: 1,
});
var points = two.makePoints(200, 200, 220, 200, 180, 200);
points.size = 10;
points.noStroke();
points.fill = '#00AEFF';
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/points' + suffix,
two.renderer,
'Two.makePoints renders properly.'
);
});
QUnit.test('Two.makePath', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 400,
height: 400,
ratio: deviceRatio,
});
var amount = 20;
var points = [];
for (var i = 0; i < amount; i++) {
var pct = i / amount;
var x = pct * 300 + 50;
var y = i % 2 ? 25 : 75;
points.push(new Two.Anchor(x, y));
}
two.makePath(points, true);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/polygon' + suffix,
two.renderer,
'Two.makePath renders properly.'
);
});
QUnit.test('Two.makeCurve', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 400,
height: 400,
ratio: deviceRatio,
});
var amount = 20;
var points = [];
for (var i = 0; i < amount; i++) {
var pct = i / amount;
var x = pct * 300 + 50;
var y = i % 2 ? 25 : 75;
points.push(new Two.Anchor(x, y));
}
two.makeCurve(points, true);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/curve' + suffix,
two.renderer,
'Two.makeCurve renders properly.'
);
});
QUnit.test('Two.makeLinearGradient', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 400,
height: 400,
ratio: deviceRatio,
});
var gradient = two.makeLinearGradient(
0,
-two.height / 2,
0,
two.height / 2,
new Two.Gradient.Stop(0, 'rgb(255, 100, 100)'),
new Two.Gradient.Stop(1, 'rgb(100, 100, 255)')
);
var rect = two.makeRectangle(
two.width / 2,
two.height / 2,
two.width / 4,
two.height / 4
);
rect.fill = gradient;
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/linear-gradient' + suffix,
two.renderer,
'Two.makeLinearGradient renders properly.'
);
});
QUnit.test('Two.makeRadialGradient', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 400,
height: 400,
ratio: deviceRatio,
});
var gradient = two.makeRadialGradient(
0.5,
0.5,
2,
new Two.Gradient.Stop(0, 'rgb(255, 100, 100)'),
new Two.Gradient.Stop(1, 'rgb(100, 100, 255)')
);
var rect = two.makeRectangle(
two.width / 2,
two.height / 2,
two.width / 4,
two.height / 4
);
rect.fill = gradient;
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/radial-gradient' + suffix,
two.renderer,
'Two.makeLinearGradient renders properly.'
);
});
QUnit.test('two.makeImage (Simple)', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 200,
height: 100,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(path, two.width / 2, two.height / 2, 200, 100);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-fill' + suffix,
two.renderer,
'Two.makeImage renders properly with default fill mode.'
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode: fit)', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 200,
height: 100,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
200,
100,
'fit'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-fit' + suffix,
two.renderer,
'Two.makeImage renders properly in fit mode.'
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode: fill)', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 200,
height: 100,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
200,
100,
'fill'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-fill' + suffix,
two.renderer,
'Two.makeImage renders properly in fill mode.'
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode: crop)', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 200,
height: 100,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
200,
100,
'crop'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-crop' + suffix,
two.renderer,
'Two.makeImage renders properly in crop mode.'
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode: tile)', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 200,
height: 100,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
200,
100,
'tile'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-tile' + suffix,
two.renderer,
'Two.makeImage renders properly in tile mode.'
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode: stretch)', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 200,
height: 100,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
200,
100,
'stretch'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-stretch' + suffix,
two.renderer,
'Two.makeImage renders properly in stretch mode.'
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode switching)', function (assert) {
assert.expect(2);
assert.done = assert.async(2);
var two = new Two({
type: Two.Types.canvas,
width: 200,
height: 100,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
200,
100,
'fit'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-fit' + suffix,
two.renderer,
'Two.makeImage renders properly in initial fit mode.',
function () {
image.mode = 'fill';
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-fill' + suffix,
two.renderer,
'Two.Image changes mode properly from fit to fill.'
);
}
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeSprite (Simple)', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 400,
height: 400,
ratio: deviceRatio,
});
var path = '/tests/images/sequence/00000.png';
var sprite = two.makeSprite(path, two.width / 2, two.height / 2);
var texture = sprite.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/sprite-simple' + suffix,
two.renderer,
'Two.makeSprite renders properly.'
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
});
QUnit.test('two.makeImageSequence', function (assert) {
assert.expect(2);
assert.done = assert.async(2);
var two = new Two({
type: Two.Types.canvas,
width: 400,
height: 400,
ratio: deviceRatio,
});
var paths = [];
for (var i = 0; i < 30; i++) {
paths.push('/tests/images/sequence/' + QUnit.Utils.digits(i, 5) + '.png');
}
var sequence = two.makeImageSequence(
paths,
two.width / 2,
two.height / 2,
2
);
sequence.index = 3;
var texture = sequence.textures[sequence.index];
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-sequence-1' + suffix,
two.renderer,
'Two.ImageSequence applied the correct texture properly.',
function () {
sequence.index = 7;
texture = sequence.textures[sequence.index];
texture._flagImage = true;
texture.bind(Two.Events.Types.load, function () {
texture.unbind(Two.Events.Types.load);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-sequence-2' + suffix,
two.renderer,
'Two.ImageSequence can change index properly.'
);
});
texture._update();
}
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
two.renderer.domElement.style.cursor = 'pointer';
two.renderer.domElement.addEventListener(
'click',
function () {
if (two.playing) {
two.pause();
} else {
sequence.loop = true;
sequence.play();
two.play();
}
},
false
);
});
QUnit.test('two.makeSprite', function (assert) {
assert.expect(2);
assert.done = assert.async(2);
var two = new Two({
type: Two.Types.canvas,
width: 400,
height: 400,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var sprite = two.makeSprite(
path,
two.width / 2,
two.height / 2,
4,
4,
2,
false
);
var texture = sprite.texture;
sprite.index = 3;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-sequence-1' + suffix,
two.renderer,
'Two.makeSprite renders properly.',
function () {
sprite.index = 7;
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-sequence-2' + suffix,
two.renderer,
'Two.Sprite changed index properly.'
);
}
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
two.renderer.domElement.style.cursor = 'pointer';
two.renderer.domElement.addEventListener(
'click',
function () {
if (two.playing) {
two.pause();
} else {
sprite.loop = true;
sprite.play();
two.play();
}
},
false
);
});
QUnit.test('Two.makeText', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 400,
height: 400,
ratio: deviceRatio,
});
var text = two.makeText('Hello World', two.width / 2, two.height / 2);
text.fill = '#00aeff';
text.noStroke();
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/text' + suffix,
two.renderer,
'Two.makeText renders properly.'
);
});
QUnit.test('Styles', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.canvas,
width: 400,
height: 400,
ratio: deviceRatio,
});
var shape = two.makeRectangle(two.width / 2, two.height / 2, 50, 50);
shape.rotation = Math.PI / 2;
shape.scale = 0.5;
shape.fill = 'lightcoral';
shape.stroke = '#333';
shape.linewidth = 10;
shape.opacity = 0.5;
shape.join = 'miter';
shape.cap = 'butt';
shape.miter = 10;
shape.closed = false;
shape.curved = true;
shape.visible = false;
shape.visible = true;
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/styles' + suffix,
two.renderer,
'Styles render properly.'
);
});
})();
================================================
FILE: tests/suite/core.js
================================================
/**
* Tests Two.js Core Classes Functionality:
* + Two.Utils ( Underscore Methods )
* + Two.Events
* + Two.Vector
* + Two.Matrix
* + Two.Collection
* + Two.Shape
* + Two.Registry
* + Two.Texture
* + Two
*/
QUnit.module('Core');
QUnit.test('Two.Events', function (assert) {
assert.expect(1);
var item = new Two.Events();
item.bind('change', function (message) {
assert.equal(message, 'hello', 'Bound Two.Events successfully.');
});
item.trigger('change', 'hello');
item.unbind('change');
item.trigger('change');
});
QUnit.test('Two.Vector', function (assert) {
assert.expect(48);
var vector = new Two.Anchor();
assert.equal(vector.x, 0, 'x property defaults to 0.');
assert.equal(vector.y, 0, 'y property defaults to 0.');
vector.x = 5;
vector.y = 5;
assert.equal(vector.x, 5, 'x property can be set properly.');
assert.equal(vector.y, 5, 'y property can be set properly.');
vector.set(10, 10);
assert.equal(vector.x, 10, 'Two.Vector.set applies x value properly.');
assert.equal(vector.y, 10, 'Two.Vector.set applies y value properly.');
vector.copy({ x: 20, y: 20 });
assert.equal(vector.x, 20, 'Two.Vector.copy applies x value properly.');
assert.equal(vector.y, 20, 'Two.Vector.copy applies y value properly.');
vector.clear();
assert.equal(vector.x, 0, 'Two.Vector.clear applies x value properly.');
assert.equal(vector.y, 0, 'Two.Vector.clear applies y value properly.');
vector.set(10, 10);
var clone = vector.clone();
assert.ok(
clone instanceof Two.Vector,
'Two.Vector.clone returns an instance of Two.Vector.'
);
assert.equal(clone.x, vector.x, 'Two.Vector.clone applies x value properly.');
assert.equal(clone.y, vector.y, 'Two.Vector.clone applies y value properly.');
vector.add({ x: 5, y: 5 });
assert.equal(vector.x, 15, 'Two.Vector.add applies x value properly.');
assert.equal(vector.y, 15, 'Two.Vector.add applies y value properly.');
vector.addSelf({ x: 5, y: 5 });
assert.equal(vector.x, 20, 'Two.Vector.addSelf applies x value properly.');
assert.equal(vector.y, 20, 'Two.Vector.addSelf applies y value properly.');
vector.sub({ x: 15, y: 15 });
assert.equal(vector.x, 5, 'Two.Vector.sub applies x value properly.');
assert.equal(vector.y, 5, 'Two.Vector.sub applies y value properly.');
vector.subSelf({ x: 5, y: 5 });
assert.equal(vector.x, 0, 'Two.Vector.subSelf applies x value properly.');
assert.equal(vector.y, 0, 'Two.Vector.subSelf applies y value properly.');
vector.set(2.5, 2.5);
vector.multiplySelf({ x: 2, y: 2 });
assert.equal(
vector.x,
5,
'Two.Vector.multiplySelf applies x value properly.'
);
assert.equal(
vector.y,
5,
'Two.Vector.multiplySelf applies y value properly.'
);
vector.multiplyScalar(2);
assert.equal(
vector.x,
10,
'Two.Vector.multiplyScalar applies x value properly.'
);
assert.equal(
vector.y,
10,
'Two.Vector.multiplyScalar applies y value properly.'
);
vector.divideScalar(2);
assert.equal(
vector.x,
5,
'Two.Vector.divideScalar applies x value properly.'
);
assert.equal(
vector.y,
5,
'Two.Vector.divideScalar applies y value properly.'
);
vector.divideScalar();
assert.equal(
vector.x,
5,
'Two.Vector.divideScalar applies x value properly.'
);
assert.equal(
vector.y,
5,
'Two.Vector.divideScalar applies y value properly.'
);
vector.set(1, -1);
vector.negate();
assert.equal(vector.x, -1, 'Two.Vector.negate applies x value properly.');
assert.equal(vector.y, 1, 'Two.Vector.negate applies y value properly.');
assert.equal(
vector.dot({ x: 5, y: 5 }),
0,
'Two.Vector.dot returns correct result.'
);
vector.set(5, 5);
assert.equal(
vector.lengthSquared(),
50,
'Two.Vector.lengthSquared returns correct result.'
);
assert.equal(
vector.length(),
Math.sqrt(50),
'Two.Vector.length returns correct result.'
);
vector.normalize();
assert.equal(
vector.x,
5 / Math.sqrt(50),
'Two.Vector.normalize applies x value properly.'
);
assert.equal(
vector.y,
5 / Math.sqrt(50),
'Two.Vector.normalize applies y value properly.'
);
vector.set(0, 0);
clone.set(5, 5);
assert.equal(
vector.distanceToSquared(clone),
50,
'Two.Vector.distanceToSquared returns correct result.'
);
assert.equal(
vector.distanceTo(clone),
Math.sqrt(50),
'Two.Vector.distanceTo, returns correct result.'
);
vector.set(1, 1).setLength(5);
assert.equal(
vector.x,
5 / Math.sqrt(2),
'Two.Vector.setLength applies x value properly.'
);
assert.equal(
vector.y,
5 / Math.sqrt(2),
'Two.Vector.setLength applies x value properly.'
);
vector.set(1, 1);
assert.equal(
vector.equals({ x: 1, y: 1 }),
true,
'Two.Vector.equals returns correct result.'
);
vector.lerp({ x: 5, y: 5 }, 0.5);
assert.equal(
vector.x,
(5 - 1) * 0.5 + 1,
'Two.Vector.lerp applies x value properly.'
);
assert.equal(
vector.y,
(5 - 1) * 0.5 + 1,
'Two.Vector.lerp applies y value properly.'
);
vector.clear();
assert.equal(
vector.isZero(),
true,
'Two.Vector.isZero returns correct result.'
);
vector.set(9, 3);
vector.rotate(Math.PI / 2);
assert.equal(
vector.equals({ x: -2.9999999999999996, y: 9 }),
true,
'Two.Vector.rotate applies x, y properly.'
);
vector.rotate(Math.PI / 2);
assert.equal(
vector.equals({ x: -9, y: -2.999999999999999 }),
true,
'Two.Vector.rotate applies x, y properly.'
);
vector.rotate(Math.PI / 2);
assert.equal(
vector.equals({ x: 2.9999999999999987, y: -9 }),
true,
'Two.Vector.rotate applies x, y properly.'
);
vector.rotate(Math.PI / 2);
assert.equal(
vector.equals({ x: 9, y: 2.9999999999999982 }),
true,
'Two.Vector.rotate applies x, y properly.'
);
});
QUnit.test('Bound Two.Vector', function (assert) {
assert.expect(45);
var vector = new Two.Anchor();
vector.bind(Two.Events.Types.change, function () {});
assert.equal(vector._bound, true, 'Vector is bound.');
assert.equal(vector.x, 0, 'x property defaults to 0.');
assert.equal(vector.y, 0, 'y property defaults to 0.');
vector.x = 5;
vector.y = 5;
assert.equal(vector.x, 5, 'x property can be set properly.');
assert.equal(vector.y, 5, 'y property can be set properly.');
vector.set(10, 10);
assert.equal(vector.x, 10, 'Two.Vector.set applies x value properly.');
assert.equal(vector.y, 10, 'Two.Vector.set applies y value properly.');
vector.copy({ x: 20, y: 20 });
assert.equal(vector.x, 20, 'Two.Vector.copy applies x value properly.');
assert.equal(vector.y, 20, 'Two.Vector.copy applies y value properly.');
vector.clear();
assert.equal(vector.x, 0, 'Two.Vector.clear applies x value properly.');
assert.equal(vector.y, 0, 'Two.Vector.clear applies y value properly.');
vector.set(10, 10);
var clone = vector.clone();
assert.ok(
clone instanceof Two.Vector,
'Two.Vector.clone returns an instance of Two.Vector.'
);
assert.equal(clone.x, vector.x, 'Two.Vector.clone applies x value properly.');
assert.equal(clone.y, vector.y, 'Two.Vector.clone applies y value properly.');
vector.add({ x: 5, y: 5 });
assert.equal(vector.x, 15, 'Two.Vector.add applies x value properly.');
assert.equal(vector.y, 15, 'Two.Vector.add applies y value properly.');
vector.addSelf({ x: 5, y: 5 });
assert.equal(vector.x, 20, 'Two.Vector.addSelf applies x value properly.');
assert.equal(vector.y, 20, 'Two.Vector.addSelf applies y value properly.');
vector.sub({ x: 15, y: 15 });
assert.equal(vector.x, 5, 'Two.Vector.sub applies x value properly.');
assert.equal(vector.y, 5, 'Two.Vector.sub applies y value properly.');
vector.subSelf({ x: 5, y: 5 });
assert.equal(vector.x, 0, 'Two.Vector.subSelf applies x value properly.');
assert.equal(vector.y, 0, 'Two.Vector.subSelf applies y value properly.');
vector.set(2.5, 2.5);
vector.multiplySelf({ x: 2, y: 2 });
assert.equal(
vector.x,
5,
'Two.Vector.multiplySelf applies x value properly.'
);
assert.equal(
vector.y,
5,
'Two.Vector.multiplySelf applies y value properly.'
);
vector.multiplyScalar(2);
assert.equal(
vector.x,
10,
'Two.Vector.multiplyScalar applies x value properly.'
);
assert.equal(
vector.y,
10,
'Two.Vector.multiplyScalar applies y value properly.'
);
vector.divideScalar(2);
assert.equal(
vector.x,
5,
'Two.Vector.divideScalar applies x value properly.'
);
assert.equal(
vector.y,
5,
'Two.Vector.divideScalar applies y value properly.'
);
vector.divideScalar();
assert.equal(
vector.x,
5,
'Two.Vector.divideScalar applies x value properly.'
);
assert.equal(
vector.y,
5,
'Two.Vector.divideScalar applies y value properly.'
);
vector.set(1, -1);
vector.negate();
assert.equal(vector.x, -1, 'Two.Vector.negate applies x value properly.');
assert.equal(vector.y, 1, 'Two.Vector.negate applies y value properly.');
assert.equal(
vector.dot({ x: 5, y: 5 }),
0,
'Two.Vector.dot returns correct result.'
);
vector.set(5, 5);
assert.equal(
vector.lengthSquared(),
50,
'Two.Vector.lengthSquared returns correct result.'
);
assert.equal(
vector.length(),
Math.sqrt(50),
'Two.Vector.length returns correct result.'
);
vector.normalize();
assert.equal(
vector.x,
5 / Math.sqrt(50),
'Two.Vector.normalize applies x value properly.'
);
assert.equal(
vector.y,
5 / Math.sqrt(50),
'Two.Vector.normalize applies y value properly.'
);
vector.set(0, 0);
clone.set(5, 5);
assert.equal(
vector.distanceToSquared(clone),
50,
'Two.Vector.distanceToSquared returns correct result.'
);
assert.equal(
vector.distanceTo(clone),
Math.sqrt(50),
'Two.Vector.distanceTo, returns correct result.'
);
vector.set(1, 1).setLength(5);
assert.equal(
vector.x,
5 / Math.sqrt(2),
'Two.Vector.setLength applies x value properly.'
);
assert.equal(
vector.y,
5 / Math.sqrt(2),
'Two.Vector.setLength applies x value properly.'
);
vector.set(1, 1);
assert.equal(
vector.equals({ x: 1, y: 1 }),
true,
'Two.Vector.equals returns correct result.'
);
vector.lerp({ x: 5, y: 5 }, 0.5);
assert.equal(
vector.x,
(5 - 1) * 0.5 + 1,
'Two.Vector.lerp applies x value properly.'
);
assert.equal(
vector.y,
(5 - 1) * 0.5 + 1,
'Two.Vector.lerp applies y value properly.'
);
vector.clear();
assert.equal(
vector.isZero(),
true,
'Two.Vector.isZero returns correct result.'
);
});
QUnit.test('Two.Matrix', function (assert) {
assert.expect(12);
var matrix = new Two.Matrix();
var check = true;
matrix.set(0, 0, 0, 0, 0, 0, 0, 0, 0);
for (var i = 0; i < matrix.elements.length; i++) {
var el = matrix.elements[i];
if (el !== 0) {
check = false;
break;
}
}
assert.equal(check, true, 'Two.Matrix.set applies elements properly.');
matrix.identity();
var identity = [1, 0, 0, 0, 1, 0, 0, 0, 1];
check = true;
for (var i = 0; i < matrix.elements.length; i++) {
var a = matrix.elements[i];
var b = identity[i];
if (a !== b) {
check = false;
break;
}
}
assert.equal(check, true, 'Two.Matrix.identity applies elements properly.');
matrix.set(1, 2, 3, 4, 5, 6, 7, 8, 9);
matrix.multiply(9, 8, 7, 6, 5, 4, 3, 2, 1);
var result = [30, 24, 18, 84, 69, 54, 138, 114, 90];
check = true;
for (var i = 0; i < matrix.elements.length; i++) {
var a = matrix.elements[i];
var b = result[i];
if (a !== b) {
check = false;
break;
}
}
assert.equal(
check,
true,
'Two.Matrix.multiply applies elements properly when multiplied by another matrix.'
);
matrix.set(1, 2, 3, 4, 5, 6, 7, 8, 9);
var vector = matrix.multiply(9, 8, 7);
result = [46, 118, 190];
assert.deepEqual(
vector,
result,
'Two.Matrix.multiply applies elements properly when multipled by a vector.'
);
matrix.set(1, 1, 1, 1, 1, 1, 1, 1, 1);
matrix.multiply(9);
check = true;
for (var i = 0; i < matrix.elements.length; i++) {
var el = matrix.elements[i];
if (el !== 9) {
check = false;
break;
}
}
assert.equal(
check,
true,
'Two.Matrix.multiply applies elements properly when multiplied by a scalar.'
);
matrix.set(1, 2, 3, 4, 5, 6, 7, 8, 9);
assert.deepEqual(
matrix.toTransformArray(),
[1, 4, 2, 5, 3, 6],
'Two.Matrix.toTransformArray returns correct result for 6 digit transformation.'
);
assert.deepEqual(
matrix.toTransformArray(true),
[1, 4, 7, 2, 5, 8, 3, 6, 9],
'Two.Matrix.toTransformArray returns correct result for 9 digit transformation.'
);
assert.deepEqual(
matrix.toArray(),
[1, 2, 3, 4, 5, 6],
'Two.Matrix.toArray returns correct result for 6 digit transformation.'
);
assert.deepEqual(
matrix.toArray(true),
[1, 2, 3, 4, 5, 6, 7, 8, 9],
'Two.Matrix.toArray returns correct result for 9 digit transformation.'
);
assert.equal(
matrix.toString(),
'1 4 2 5 3 6',
'Two.Matrix.toString returns correct result for 6 digit transformation.'
);
var clone = matrix.clone();
assert.ok(
clone instanceof Two.Matrix,
'Two.Matrix.clone returns instance of Two.Matrix.'
);
assert.deepEqual(
clone.elements,
matrix.elements,
'Two.Matrix.clone applies elements properly.'
);
});
QUnit.test('Two.Collection', function (assert) {
assert.expect(19);
var poly = new Two.Path([new Two.Anchor(0, 0)]);
var vector = new Two.Anchor(150, 150);
var vertices = poly.vertices;
var removed;
assert.equal(
vertices instanceof Two.Collection,
true,
'Polyon.vertices is an instance of Two.Collection'
);
assert.equal(
vertices[0].equals(new Two.Anchor(0, 0)),
true,
'Two.Collection created with correct items'
);
vertices.push(vector);
assert.equal(
vertices.length,
2,
'Two.Collection.push added one item to the end of vertices collection'
);
removed = vertices.pop();
assert.equal(
vertices.length,
1,
'Two.Collection.pop removed one item from the end of the vertices collection'
);
assert.equal(
removed.equals(vector),
true,
'Two.Collection.push removed the correct item'
);
// Clear removed to reuse
removed = null;
vertices.unshift(vector);
assert.equal(
vertices.length,
2,
'Two.Collection.unshift added one item to the front of the vertices collection'
);
removed = vertices.shift();
assert.equal(
vertices.length,
1,
'Two.Collection.shift removed one item from the front of the vertices collection'
);
assert.equal(
removed.equals(vector),
true,
'Two.Collection.shift removed the correct item'
);
// Clear removed to reuse
removed = null;
vertices.push(
new Two.Anchor(1, 1),
new Two.Anchor(2, 2),
new Two.Anchor(3, 3),
new Two.Anchor(4, 4)
);
assert.equal(
vertices.length,
5,
'Two.Collection.push adds several items to the end of vertices collection'
);
removed = vertices.splice(2, 1, vector);
assert.equal(
vertices.length,
5,
'Two.Collection.splice adds and removes items from the vertices collection'
);
assert.equal(
removed[0].equals(new Two.Anchor(2, 2)),
true,
'Two.Collection.splice remove the correct items from the vertices collection'
);
assert.equal(
vertices[2].equals(vector),
true,
'Two.Collection.splice inserts correct item to the middle of the vertices collection'
);
var a = new Two.Collection('a', 'b', 'c', 'd', 'e');
assert.equal(
a.slice(1, 2)[0],
'b',
'Two.Collection.slice does correct beginning / end index selection.'
);
a.splice(0, 0, 'z');
assert.equal(
a[0],
'z',
'Two.Collection.splice correctly inserts properties.'
);
var children = new Two.Group.Children('a', 'b', 'c', 'd', 'e');
var result = children.map(function (v) {
return v;
});
assert.equal(
result.length,
5,
'Two.Collection.map correctly iterates through the necessary items.'
);
const emptyCollection = new Two.Collection();
assert.equal(
emptyCollection.length,
0,
'Two.Collection correctly enumerates properties.'
);
assert.equal(
Object.keys(emptyCollection).length,
0,
'Two.Collection correctly Object.keys an empty collection.'
);
const included = emptyCollection.filter(() => true);
console.log(
included.length,
0,
'Two.Collection.filter correctly iterates over items.'
);
emptyCollection.push(5, 10, 432, 90);
assert.equal(
Object.keys(emptyCollection).length,
4,
'Two.Collection correctly Object.keys a populated list collection.'
);
assert.equal(
emptyCollection.find((v) => v === 5),
5,
'Two.Collection.find correctly iterates over items.'
);
});
QUnit.test('Two.Children', function (assert) {
assert.expect(4);
var group = new Two.Group();
var shape = new Two.Shape();
group.add(shape);
shape._update(true);
assert.equal(
shape.id in group.children.ids,
true,
'Two.Children properly adds child elements to list.'
);
group.remove(shape);
group._update(true);
assert.equal(
shape.id in group.children.ids,
false,
'Two.Children properly removes child element from list.'
);
group.add(shape);
shape.id = 'custom';
shape._update(true);
assert.equal(
shape.id in group.children.ids,
true,
'Two.Children properly updates ids map when child id changes.'
);
try {
// Check to be able to add and remove masks freely without errors
group.mask = new Two.Rectangle(0, 0, 10, 10);
group.mask = null;
assert.equal(
group.mask,
null,
'Two.Group properly assigns and unassigns masks.'
);
} catch (e) {
console.error(e);
}
});
QUnit.test('Two.Shape', function (assert) {
assert.expect(13);
var shape = new Two.Shape();
assert.equal(
shape.translation.toString(),
'0, 0',
'Two.Shape.translation constructed properly.'
);
assert.equal(shape.scale, 1, 'Two.Shape.scale constructed properly.');
assert.equal(shape.rotation, 0, 'Two.Shape.rotation constructed properly.');
shape.translation.x = 50;
shape.translation.y = 25;
shape._update();
assert.equal(
shape._matrix.toString(),
'1 0 0 1 50 25',
'Two.Shape.translation binds properly.'
);
shape.translation = new Two.Anchor(25, 50);
shape._update();
assert.equal(
shape._matrix.toString(),
'1 0 0 1 25 50',
'Two.Shape.translation binds properly.'
);
shape.translation.x = 0;
shape.translation.y = 0;
shape._update();
assert.equal(
shape._matrix.toString(),
'1 0 0 1 0 0',
'Two.Shape.translation binds properly.'
);
shape.scale = 3;
shape._update();
assert.equal(
shape._matrix.toString(),
'3 0 0 3 0 0',
'Two.Shape.scale uniform scale works properly.'
);
shape.scale = new Two.Anchor(1, 2);
shape._update();
assert.equal(
shape._matrix.toString(),
'1 0 0 2 0 0',
'Two.Shape.scale 2 dimension scale works properly.'
);
shape.scale.x = 2;
shape.scale.y = 1;
shape._update();
assert.equal(
shape._matrix.toString(),
'2 0 0 1 0 0',
'Two.Shape.scale 2 dimension scale binds properly for event listening.'
);
var s = shape.scale;
shape.scale = 10;
s.x = 5;
shape._update();
assert.equal(
shape._matrix.toString(),
'10 0 0 10 0 0',
'Two.Shape.scale 2 dimension scale unbinds properly.'
);
shape.rotation = 3.14;
shape._update();
assert.equal(
shape._matrix.toString(),
'-9.999988 0.015926 -0.015927 -9.999988 0 0',
'Two.Shape.rotation works properly.'
);
assert.equal(
shape.worldMatrix.toString(),
'-9.999988 0.015926 -0.015927 -9.999988 0 0',
'Two.Shape.worldMatrix locally updates properly.'
);
var scene = new Two.Group();
scene.add(shape);
scene.position.x = 100;
scene._update();
assert.equal(
shape.worldMatrix.toString(),
'-9.999988 0.015926 -0.015927 -9.999988 100 0',
'Two.Shape.worldMatrix properly calculates world matrix.'
);
});
QUnit.test('Children adding and removing', function (assert) {
assert.expect(28);
var group1 = new Two.Group();
var group2 = new Two.Group();
var group3 = new Two.Group();
var poly1 = new Two.Path([new Two.Anchor(0, 0)]);
var poly2 = new Two.Path([new Two.Anchor(0, 0)]);
var poly3 = new Two.Path([new Two.Anchor(0, 0)]);
var poly4 = new Two.Path([new Two.Anchor(0, 0)]);
var poly5 = new Two.Path([new Two.Anchor(0, 0)]);
poly1.addTo(group1);
assert.equal(
poly1,
group1.children[0],
'Can add objects to group (via object)'
);
assert.equal(group1, poly1.parent, 'Can add objects to group (via object)');
assert.ok(
~poly1.parent.additions.indexOf(poly1),
'Can add objects to group (via object)'
);
assert.equal(group1.children.length, 1, 'Correct childrens length');
group2.add(poly2);
assert.equal(
poly2,
group2.children[0],
'Can add objects to group (via group)'
);
assert.equal(group2, poly2.parent, 'Can add objects to group (via group)');
assert.ok(
~poly2.parent.additions.indexOf(poly2),
'Can add objects to group (via group)'
);
assert.equal(group2.children.length, 1, 'Correct childrens length');
group1.add(poly2);
assert.equal(poly2, group1.children[1], 'Can reassign objects to group');
assert.equal(group1, poly2.parent, 'Can reassign objects to group');
assert.ok(
~poly2.parent.additions.indexOf(poly2),
'Can reassign objects to group'
);
assert.ok(!~group2.additions.indexOf(poly2), 'Can reassign objects to group');
assert.equal(group1.children.length, 2, 'Correct childrens length');
assert.equal(group2.children.length, 0, 'Correct childrens length');
group3.add(group1);
assert.equal(group1, group3.children[0], 'Can add groups to group');
assert.equal(group3, group1.parent, 'Can add groups to group');
assert.ok(~group3.additions.indexOf(group1), 'Can add groups to group');
assert.equal(group3.children.length, 1, 'Correct childrens length');
group1.add(poly3);
group1.add(poly4);
group1.add(poly5);
assert.equal(group1.children.length, 5, 'Correct childrens length');
group3.add(group1.children);
assert.equal(
group3.children.length,
6,
'Can reassign children to another group'
);
assert.equal(
group1.children.length,
0,
'Can reassign children to another group'
);
assert.equal(
group1.additions.length,
0,
'Can reassign children to another group'
);
assert.equal(poly5.parent, group3, 'Can reassign children to another group');
assert.ok(
~poly5.parent.additions.indexOf(poly5),
'Can reassign children to another group'
);
group3.remove(poly4);
assert.equal(group3.children.length, 5, 'Can remove objects from group');
assert.equal(group3.additions.length, 5, 'Can remove objects from group');
assert.equal(poly4.parent, null, 'Can remove objects from group');
group3.add(void 0);
assert.ok(true, 'Can safely add undefined stuff to group');
});
QUnit.test('Two.Registry', function (assert) {
assert.expect(4);
var registry = new Two.Registry();
var id = 'foo';
var val = 'bar';
registry.add(id, val);
assert.equal(
registry.get(id),
val,
'Two.Registry registers key values properly.'
);
assert.equal(
registry.contains(id),
true,
'Two.Registry.contains found registered value properly.'
);
registry.remove(id);
assert.equal(
registry.map[id],
undefined,
'Two.Registry removes key values properly.'
);
assert.equal(
registry.contains(id),
false,
'Two.Registry.contains did not find removed id.'
);
});
QUnit.test('Two.Texture', function (assert) {
assert.expect(7);
assert.done = assert.async(2);
// Test Image Loading to Texture
var root = '/tests/images/sequence/';
var path = [root, '00000.png'].join('');
var ta = new Two.Texture(path, function () {
assert.ok(true, 'Two.Texture callback on load triggered properly.');
tc._update();
assert.equal(
ta.image,
tc.image,
'Two.Texture applies image properties properly after undefined.'
);
assert.ok(
tc.loaded,
'Two.Texture applies loaded property correctly on undefined source images.'
);
assert.done();
});
ta.image.id = 'first-original-image';
var image = document.createElement('img');
var onload = function () {
var tb = new Two.Texture(image);
var absolutePath = [
window.location.protocol,
'//',
window.location.host,
path,
].join('');
assert.equal(
tb.src,
absolutePath,
'Two.Texture takes in image and applies path properly.'
);
assert.equal(
tb.image,
ta.image,
'Two.Texture takes in image and applies registered image tag properly.'
);
assert.equal(
tb.loaded,
true,
'Two.Texture takes in image and applies loaded property properly.'
);
assert.done();
image.onload = null;
};
image.onload = onload;
image.src = path;
image.id = 'second-same-image-as-first';
var tc = new Two.Texture();
assert.ok(
true,
'Two.Texture able to be constructed without any parameters properly.'
);
tc.src = path;
});
QUnit.test('Two', function (assert) {
assert.expect(8);
var selector = QUnit.Utils.getSelector(assert.test);
var elem = document.querySelector(selector);
elem.id = assert.test.id + '-container';
var two = new Two().appendTo(elem);
assert.equal(
two.renderer.domElement.parentElement.id,
elem.id,
'Two appends to the correct element when `appendTo` invoked.'
);
two.update();
assert.equal(two.frameCount, 1, 'Two increments frameCount correctly.');
two.play();
assert.ok(two.playing, 'Two.Utils.setPlaying applied correctly.');
assert.ok(
typeof Two.NextFrameId === 'number',
'requestAnimationFrame runs correctly.'
);
two.pause();
assert.ok(!two.playing, 'Two.pause correctly stops updating.');
var rectangle = two.makeRectangle(two.width / 2, two.height / 2, 10, 10);
assert.equal(two.scene.children.length, 1, 'Adds children correctly.');
two.remove(rectangle);
assert.equal(two.scene.children.length, 0, 'Removes children correctly');
two.add(rectangle);
two.clear();
assert.equal(two.scene.children.length, 0, 'Clears children correctly');
});
QUnit.test('Two.Path Object Conversion', function (assert) {
assert.expect(9);
// Create an original path with some properties
var path = new Two.Path([new Two.Anchor(0, 0), new Two.Anchor(100, 100)]);
path.fill = '#ff0000';
path.stroke = '#00ff00';
path.linewidth = 5;
path.opacity = 0.5;
path.visible = true;
path.cap = 'round';
path.join = 'miter';
path.miter = 10;
path.id = 'my-path';
// Convert to object
var obj = path.toObject();
// Test object has expected properties
assert.equal(typeof obj, 'object', 'Two.Path.toObject creates an object');
assert.equal(obj.vertices.length, 2, 'Two.Path.toObject preserves vertices');
assert.equal(obj.fill, '#ff0000', 'Two.Path.toObject preserves fill');
assert.equal(obj.id, 'my-path', 'Two.Path.toObject preserves id');
// Create new path from object
var newPath = Two.Path.fromObject(obj);
// Test new path matches original
assert.equal(newPath.fill, path.fill, 'Two.Path.fromObject preserves fill');
assert.equal(
newPath.stroke,
path.stroke,
'Two.Path.fromObject preserves stroke'
);
assert.equal(
newPath.vertices.length,
path.vertices.length,
'Two.Path.fromObject preserves vertices length'
);
assert.equal(newPath.id, path.id, 'Two.Path.fromObject preserves id');
// Test copy method
var copiedPath = new Two.Path().copy(path);
assert.deepEqual(
{ ...copiedPath.toObject(), id: path.id },
obj,
'Two.Path.copy creates identical path'
);
});
QUnit.test('Two.Path strokeAttenuation', function (assert) {
assert.expect(4);
var path = new Two.Path([new Two.Anchor(0, 0), new Two.Anchor(100, 100)]);
path.strokeAttenuation = true;
assert.equal(
path.strokeAttenuation,
true,
'Can get property strokeAttenuation correctly.'
);
assert.equal(
path._strokeAttenuation,
true,
'Can set property strokeAttenuation correctly.'
);
path.strokeAttenuation = false;
assert.equal(
path.strokeAttenuation,
false,
'Can set property strokeAttenuation to false correctly.'
);
assert.equal(
path._strokeAttenuation,
false,
'Can get property strokeAttenuation false correctly.'
);
});
QUnit.test('Two.Group Object Conversion', function (assert) {
assert.expect(6);
// Create original group with children
var group = new Two.Group();
var circle = new Two.Circle(0, 0, 50);
var rect = new Two.Rectangle(100, 100, 50, 50);
group.add(circle, rect);
group.id = 'my-group';
// Convert to object
var obj = group.toObject();
// Test object has expected properties
assert.equal(typeof obj, 'object', 'Two.Group.toObject creates an object');
assert.equal(obj.children.length, 2, 'Two.Group.toObject preserves children');
assert.equal(obj.id, 'my-group', 'Two.Group.toObject preserves id');
// Create new group from object
var newGroup = Two.Group.fromObject(obj);
// Test new group matches original
assert.equal(
newGroup.children.length,
group.children.length,
'Two.Group.fromObject preserves children length'
);
assert.equal(newGroup.id, group.id, 'Two.Group.fromObject preserves id');
// Test child types are preserved
assert.ok(
newGroup.children[0] instanceof Two.Circle,
'Two.Group.fromObject preserves child types'
);
// TODO: Implement
// // Test copy method
// var copiedGroup = new Two.Group().copy(group);
// copiedGroup._update();
// assert.deepEqual(
// copiedGroup.toObject(),
// group.toObject(),
// 'Two.Group.copy creates identical group'
// );
});
QUnit.test('Two.Group strokeAttenuation', function (assert) {
assert.expect(4);
var group = new Two.Group();
group.strokeAttenuation = true;
assert.equal(
group.strokeAttenuation,
true,
'Can get property strokeAttenuation correctly.'
);
assert.equal(
group._strokeAttenuation,
true,
'Can set property strokeAttenuation correctly.'
);
group.strokeAttenuation = false;
assert.equal(
group.strokeAttenuation,
false,
'Can set property strokeAttenuation to false correctly.'
);
assert.equal(
group._strokeAttenuation,
false,
'Can get property strokeAttenuation false correctly.'
);
});
QUnit.test('Two.Group.getBoundingClientRect(shallow)', function (assert) {
assert.expect(6);
const group = new Two.Group();
for (let i = 0; i < 3; i++) {
const x = i * 100;
const width = 100;
const height = 50;
const child = new Two.Rectangle(x, 0, width, height);
group.add(child);
}
const rect = group.getBoundingClientRect(true);
assert.equal(
rect.top,
-25.5,
'Two.Group.getBoundingClientRect(shallow) correctly calculates top property.'
);
assert.equal(
rect.bottom,
25.5,
'Two.Group.getBoundingClientRect(shallow) correctly calculates bottom property.'
);
assert.equal(
rect.left,
-50.5,
'Two.Group.getBoundingClientRect(shallow) correctly calculates left property.'
);
assert.equal(
rect.right,
250.5,
'Two.Group.getBoundingClientRect(shallow) correctly calculates right property.'
);
assert.equal(
rect.width,
301,
'Two.Group.getBoundingClientRect(shallow) correctly calculates width property.'
);
assert.equal(
rect.height,
51,
'Two.Group.getBoundingClientRect(shallow) correctly calculates height property.'
);
});
QUnit.test('Two.Text Object Conversion', function (assert) {
assert.expect(9);
var text = new Two.Text('Hello, World!', 100, 100);
text.fill = '#ff0000';
text.stroke = '#00ff00';
text.size = 20;
text.id = 'my-text';
var obj = text.toObject();
assert.equal(typeof obj, 'object', 'Two.Text.toObject creates an object');
assert.equal(obj.value, 'Hello, World!', 'Two.Text.toObject preserves value');
assert.equal(obj.fill, '#ff0000', 'Two.Text.toObject preserves fill');
assert.equal(obj.stroke, '#00ff00', 'Two.Text.toObject preserves stroke');
assert.equal(obj.size, 20, 'Two.Text.toObject preserves size');
assert.equal(obj.id, 'my-text', 'Two.Text.toObject preserves id');
var newText = Two.Text.fromObject(obj);
assert.equal(
newText.value,
text.value,
'Two.Text.fromObject preserves value'
);
assert.equal(newText.fill, text.fill, 'Two.Text.fromObject preserves fill');
var copiedText = new Two.Text().copy(text);
assert.deepEqual(
{ ...copiedText.toObject(), id: text.id },
text.toObject(),
'Two.Text.copy creates identical text'
);
});
QUnit.test('Two.Text strokeAttenuation', function (assert) {
assert.expect(4);
var text = new Two.Text('Hello, World!', 100, 100);
text.strokeAttenuation = true;
assert.equal(
text.strokeAttenuation,
true,
'Can get property strokeAttenuation correctly.'
);
assert.equal(
text._strokeAttenuation,
true,
'Can set property strokeAttenuation correctly.'
);
text.strokeAttenuation = false;
assert.equal(
text.strokeAttenuation,
false,
'Can set property strokeAttenuation to false correctly.'
);
assert.equal(
text._strokeAttenuation,
false,
'Can get property strokeAttenuation false correctly.'
);
});
================================================
FILE: tests/suite/dispose.js
================================================
/**
* Tests Two.js dispose() Methods:
* Comprehensive testing of dispose() functionality across the class hierarchy.
* Tests memory cleanup, event unbinding, renderer resource cleanup, and DOM element removal.
*/
(function () {
QUnit.module('Dispose Method');
// ===========================
// Element Tests (Base Class)
// ===========================
QUnit.test('Element.dispose - Event Unbinding', function (assert) {
assert.expect(3);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var rect = two.makeRectangle(100, 100, 50, 50);
var eventFired = false;
rect.bind('change', function () {
eventFired = true;
});
// Verify event fires before dispose
rect.trigger('change');
assert.ok(eventFired, 'Event fires before dispose');
// Reset
eventFired = false;
// Dispose
rect.dispose();
// Verify event doesn't fire after dispose
rect.trigger('change');
assert.ok(!eventFired, 'Event does not fire after dispose');
// Verify _bound flag is false
assert.ok(!rect._bound, '_bound flag is false after dispose');
});
QUnit.test('Element.dispose - SVG DOM Cleanup', function (assert) {
assert.expect(5);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var rect = two.makeRectangle(100, 100, 50, 50);
var initialType = rect._renderer.type;
two.update();
// Verify element exists in DOM before dispose
var elemBefore = two.renderer.domElement.querySelector('#' + rect.id);
assert.ok(elemBefore, 'Element exists in DOM before dispose');
assert.ok(rect._renderer.elem, '_renderer.elem exists before dispose');
// Dispose
rect.dispose();
// Verify element removed from DOM after dispose
var elemAfter = two.renderer.domElement.querySelector('#' + rect.id);
assert.ok(!elemAfter, 'Element removed from DOM after dispose');
assert.ok(!rect._renderer.elem, '_renderer.elem deleted after dispose');
assert.equal(rect._renderer.type, initialType, 'Renderer type preserved');
});
QUnit.test('Element.dispose - Renderer Type Preservation', function (assert) {
assert.expect(2);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var circle = two.makeCircle(100, 100, 25);
var initialType = circle._renderer.type;
circle.dispose();
assert.equal(
circle._renderer.type,
initialType,
'Renderer type preserved after dispose'
);
assert.ok(circle._renderer.type, 'Renderer type exists after dispose');
});
QUnit.test('Element.dispose - Returns Instance', function (assert) {
assert.expect(1);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var line = two.makeLine(0, 0, 100, 100);
var result = line.dispose();
assert.equal(result, line, 'dispose() returns the instance for chaining');
});
QUnit.test('Element.dispose - Multiple Dispose Calls', function (assert) {
assert.expect(2);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var star = two.makeStar(100, 100, 20, 50, 5);
two.update();
// First dispose
try {
star.dispose();
assert.ok(true, 'First dispose() succeeds');
} catch (e) {
assert.ok(false, 'First dispose() threw error: ' + e.message);
}
// Second dispose
try {
star.dispose();
assert.ok(true, 'Second dispose() succeeds without error');
} catch (e) {
assert.ok(false, 'Second dispose() threw error: ' + e.message);
}
});
// ===========================
// Shape Tests
// ===========================
QUnit.test('Shape.dispose - Translation Vector Unbinding', function (assert) {
assert.expect(3);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var rect = two.makeRectangle(100, 100, 50, 50);
var translationEventFired = false;
rect.translation.bind('change', function () {
translationEventFired = true;
});
// Verify event fires before dispose
rect.translation.trigger('change');
assert.ok(translationEventFired, 'Translation event fires before dispose');
// Reset
translationEventFired = false;
// Dispose
rect.dispose();
// Verify event doesn't fire after dispose
rect.translation.trigger('change');
assert.ok(
!translationEventFired,
'Translation event does not fire after dispose'
);
assert.ok(
!rect.translation._bound,
'Translation Vector _bound flag is false'
);
});
QUnit.test('Shape.dispose - Scale Vector Unbinding', function (assert) {
assert.expect(3);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var circle = two.makeCircle(100, 100, 50);
// Set scale to a Vector (non-uniform scaling)
circle.scale = new Two.Vector(2, 3);
var scaleEventFired = false;
circle.scale.bind('change', function () {
scaleEventFired = true;
});
// Verify event fires before dispose
circle.scale.trigger('change');
assert.ok(scaleEventFired, 'Scale event fires before dispose');
// Reset
scaleEventFired = false;
// Dispose
circle.dispose();
// Verify event doesn't fire after dispose
circle.scale.trigger('change');
assert.ok(!scaleEventFired, 'Scale event does not fire after dispose');
assert.ok(!circle.scale._bound, 'Scale Vector _bound flag is false');
});
QUnit.test('Shape.dispose - Inheritance Chain', function (assert) {
assert.expect(3);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var polygon = two.makePolygon(100, 100, 50, 6);
var eventFired = false;
polygon.bind('change', function () {
eventFired = true;
});
two.update();
// Verify Element dispose behavior (event unbinding)
polygon.trigger('change');
assert.ok(eventFired, 'Event fires before dispose');
polygon.dispose();
eventFired = false;
polygon.trigger('change');
assert.ok(!eventFired, 'Element.dispose() was called (events unbound)');
// Verify renderer cleanup (Element dispose behavior)
var elemAfter = two.renderer.domElement.querySelector('#' + polygon.id);
assert.ok(!elemAfter, 'Element.dispose() was called (DOM cleaned)');
});
// ===========================
// Path Tests
// ===========================
QUnit.test('Path.dispose - Vertices Collection Unbinding', function (assert) {
assert.expect(1);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var path = two.makePath(0, 0, 100, 0, 100, 100, 0, 100);
// Dispose should unbind vertices collection without error
try {
path.dispose();
assert.ok(
true,
'Path disposes and unbinds vertices collection without error'
);
} catch (e) {
assert.ok(false, 'Path dispose threw error: ' + e.message);
}
});
QUnit.test('Path.dispose - Individual Anchor Unbinding', function (assert) {
assert.expect(9);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var path = two.makePath(0, 0, 100, 0, 100, 100);
var anchor0EventFired = false;
var anchor1EventFired = false;
var anchor2EventFired = false;
path.vertices[0].bind('change', function () {
anchor0EventFired = true;
});
path.vertices[1].bind('change', function () {
anchor1EventFired = true;
});
path.vertices[2].bind('change', function () {
anchor2EventFired = true;
});
// Verify events fire before dispose
path.vertices[0].trigger('change');
path.vertices[1].trigger('change');
path.vertices[2].trigger('change');
assert.ok(anchor0EventFired, 'Anchor 0 event fires before dispose');
assert.ok(anchor1EventFired, 'Anchor 1 event fires before dispose');
assert.ok(anchor2EventFired, 'Anchor 2 event fires before dispose');
// Reset
anchor0EventFired = false;
anchor1EventFired = false;
anchor2EventFired = false;
// Dispose
path.dispose();
// Verify events don't fire after dispose
path.vertices[0].trigger('change');
path.vertices[1].trigger('change');
path.vertices[2].trigger('change');
assert.ok(!anchor0EventFired, 'Anchor 0 event does not fire after dispose');
assert.ok(!anchor1EventFired, 'Anchor 1 event does not fire after dispose');
assert.ok(!anchor2EventFired, 'Anchor 2 event does not fire after dispose');
// Verify _bound flags
assert.ok(!path.vertices[0]._bound, 'Anchor 0 _bound flag is false');
assert.ok(!path.vertices[1]._bound, 'Anchor 1 _bound flag is false');
assert.ok(!path.vertices[2]._bound, 'Anchor 2 _bound flag is false');
});
QUnit.test('Path.dispose - Control Point Unbinding', function (assert) {
assert.expect(7);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var path = two.makePath(0, 0, 100, 0, 100, 100);
path.curved = true;
var leftEventFired = false;
var rightEventFired = false;
// Bind to first vertex control points
path.vertices[0].controls.left.bind('change', function () {
leftEventFired = true;
});
path.vertices[0].controls.right.bind('change', function () {
rightEventFired = true;
});
// Verify events fire before dispose
path.vertices[0].controls.left.trigger('change');
path.vertices[0].controls.right.trigger('change');
assert.ok(leftEventFired, 'Left control point event fires before dispose');
assert.ok(
rightEventFired,
'Right control point event fires before dispose'
);
// Reset
leftEventFired = false;
rightEventFired = false;
// Dispose
path.dispose();
// Verify events don't fire after dispose
path.vertices[0].controls.left.trigger('change');
path.vertices[0].controls.right.trigger('change');
assert.ok(
!leftEventFired,
'Left control point event does not fire after dispose'
);
assert.ok(
!rightEventFired,
'Right control point event does not fire after dispose'
);
// Verify _bound flags
assert.ok(
!path.vertices[0].controls.left._bound,
'Left control point _bound flag is false'
);
assert.ok(
!path.vertices[0].controls.right._bound,
'Right control point _bound flag is false'
);
// Verify parent anchor also unbound
assert.ok(!path.vertices[0]._bound, 'Parent anchor _bound flag is false');
});
QUnit.test(
'Path.dispose - Fill Effect Disposal (Gradient)',
function (assert) {
assert.expect(2);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var gradient = two.makeLinearGradient(
0,
0,
1,
1,
new Two.Stop(0, 'red'),
new Two.Stop(1, 'blue')
);
var rect = two.makeRectangle(100, 100, 100, 100);
rect.fill = gradient;
two.update();
// Verify gradient has DOM element before dispose
var gradientElemBefore = two.renderer.domElement.querySelector(
'#' + gradient.id
);
assert.ok(
gradientElemBefore,
'Gradient element exists in DOM before path dispose'
);
// Dispose path (should call dispose on gradient)
try {
rect.dispose();
assert.ok(true, 'Path disposes with gradient fill without error');
} catch (e) {
assert.ok(false, 'Path dispose threw error: ' + e.message);
}
}
);
QUnit.test(
'Path.dispose - Stroke Effect Disposal (Texture)',
function (assert) {
assert.expect(4);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var texture = new Two.Texture('./images/canvas/line@2x.png');
var circle = two.makeCircle(100, 100, 50);
circle.stroke = texture;
circle.noFill();
two.update();
// Bind event to texture
var textureEventFired = false;
texture.bind('change', function () {
textureEventFired = true;
});
texture.trigger('change');
assert.ok(textureEventFired, 'Texture event fires before path dispose');
textureEventFired = false;
// Verify texture has renderer
assert.ok(texture._renderer, 'Texture has _renderer before dispose');
// Dispose path (should dispose texture)
circle.dispose();
// Verify texture disposed
texture.trigger('change');
assert.ok(
!textureEventFired,
'Texture event does not fire after path dispose'
);
assert.ok(
texture._renderer.type,
'Texture renderer type preserved after dispose'
);
}
);
QUnit.test('Path.dispose - String Fill/Stroke', function (assert) {
assert.expect(1);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var path = two.makePath(0, 0, 100, 0, 100, 100);
path.fill = 'red';
path.stroke = 'blue';
// Dispose path with string fill/stroke (no effect objects)
try {
path.dispose();
assert.ok(true, 'Path with string fill/stroke disposes without error');
} catch (e) {
assert.ok(
false,
'Path with string fill/stroke threw error: ' + e.message
);
}
});
QUnit.test('Path.dispose - Incomplete Collection', function (assert) {
assert.expect(1);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var path = two.makePath(0, 0, 100, 0, 100, 100);
// Simulate incomplete collection by removing unbind method
var originalUnbind = path.vertices.unbind;
delete path.vertices.unbind;
// Dispose should handle incomplete collection gracefully
try {
path.dispose();
assert.ok(
true,
'Path disposes without error when vertices collection has no unbind method'
);
} catch (e) {
assert.ok(
false,
'Path dispose threw error with incomplete collection: ' + e.message
);
}
// Restore for cleanup
path.vertices.unbind = originalUnbind;
});
// ===========================
// Group Tests
// ===========================
QUnit.test('Group.dispose - Recursive Child Disposal', function (assert) {
assert.expect(7);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var group = two.makeGroup();
var child1 = two.makeCircle(50, 50, 25);
var child2 = two.makeRectangle(150, 150, 50, 50);
group.add(child1, child2);
var child1EventFired = false;
var child2EventFired = false;
child1.bind('change', function () {
child1EventFired = true;
});
child2.bind('change', function () {
child2EventFired = true;
});
// Verify events fire before dispose
child1.trigger('change');
child2.trigger('change');
assert.ok(child1EventFired, 'Child1 event fires before group dispose');
assert.ok(child2EventFired, 'Child2 event fires before group dispose');
child1EventFired = false;
child2EventFired = false;
two.update();
// Dispose group (should recursively dispose children)
group.dispose();
// Verify children disposed
child1.trigger('change');
child2.trigger('change');
assert.ok(
!child1EventFired,
'Child1 event does not fire after group dispose'
);
assert.ok(
!child2EventFired,
'Child2 event does not fire after group dispose'
);
// Verify children DOM elements removed
var child1ElemAfter = two.renderer.domElement.querySelector(
'#' + child1.id
);
var child2ElemAfter = two.renderer.domElement.querySelector(
'#' + child2.id
);
assert.ok(!child1ElemAfter, 'Child1 DOM element removed');
assert.ok(!child2ElemAfter, 'Child2 DOM element removed');
// Verify group DOM element removed
var groupElemAfter = two.renderer.domElement.querySelector('#' + group.id);
assert.ok(!groupElemAfter, 'Group DOM element removed');
});
QUnit.test('Group.dispose - Nested Group Disposal', function (assert) {
assert.expect(9);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var rootGroup = two.makeGroup();
var subGroup1 = two.makeGroup();
var subGroup2 = two.makeGroup();
var leaf1 = two.makeCircle(50, 50, 20);
var leaf2 = two.makeRectangle(150, 50, 40, 40);
var leaf3 = two.makeLine(200, 50, 250, 100);
subGroup1.add(leaf1, leaf2);
subGroup2.add(leaf3);
rootGroup.add(subGroup1, subGroup2);
// Bind events at all levels
var leaf1EventFired = false;
var leaf2EventFired = false;
var leaf3EventFired = false;
leaf1.bind('change', function () {
leaf1EventFired = true;
});
leaf2.bind('change', function () {
leaf2EventFired = true;
});
leaf3.bind('change', function () {
leaf3EventFired = true;
});
// Verify events fire before dispose
leaf1.trigger('change');
leaf2.trigger('change');
leaf3.trigger('change');
assert.ok(leaf1EventFired, 'Leaf1 event fires before dispose');
assert.ok(leaf2EventFired, 'Leaf2 event fires before dispose');
assert.ok(leaf3EventFired, 'Leaf3 event fires before dispose');
leaf1EventFired = false;
leaf2EventFired = false;
leaf3EventFired = false;
two.update();
// Dispose root group (should recursively dispose entire tree)
rootGroup.dispose();
// Verify all events unbound
leaf1.trigger('change');
leaf2.trigger('change');
leaf3.trigger('change');
assert.ok(!leaf1EventFired, 'Leaf1 event does not fire after root dispose');
assert.ok(!leaf2EventFired, 'Leaf2 event does not fire after root dispose');
assert.ok(!leaf3EventFired, 'Leaf3 event does not fire after root dispose');
// Verify all DOM elements removed
var leaf1ElemAfter = two.renderer.domElement.querySelector('#' + leaf1.id);
var leaf2ElemAfter = two.renderer.domElement.querySelector('#' + leaf2.id);
var leaf3ElemAfter = two.renderer.domElement.querySelector('#' + leaf3.id);
assert.ok(!leaf1ElemAfter, 'Leaf1 DOM element removed');
assert.ok(!leaf2ElemAfter, 'Leaf2 DOM element removed');
assert.ok(!leaf3ElemAfter, 'Leaf3 DOM element removed');
});
QUnit.test(
'Group.dispose - Children Collection Unbinding',
function (assert) {
assert.expect(1);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var group = two.makeGroup();
var child = two.makeCircle(50, 50, 25);
group.add(child);
// Dispose should unbind children collection without error
try {
group.dispose();
assert.ok(
true,
'Group disposes and unbinds children collection without error'
);
} catch (e) {
assert.ok(false, 'Group dispose threw error: ' + e.message);
}
}
);
QUnit.test('Group.dispose - Empty Group', function (assert) {
assert.expect(1);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var emptyGroup = two.makeGroup();
// Dispose empty group
try {
emptyGroup.dispose();
assert.ok(true, 'Empty group disposes without error');
} catch (e) {
assert.ok(false, 'Empty group dispose threw error: ' + e.message);
}
});
// ===========================
// Text Tests
// ===========================
QUnit.test('Text.dispose - Fill Effect Disposal', function (assert) {
assert.expect(1);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var gradient = two.makeLinearGradient(
0,
0,
1,
1,
new Two.Stop(0, '#ff0000'),
new Two.Stop(1, '#0000ff')
);
var text = two.makeText('Hello', 100, 100);
text.fill = gradient;
two.update();
// Dispose text (should dispose gradient)
try {
text.dispose();
assert.ok(true, 'Text disposes with gradient fill without error');
} catch (e) {
assert.ok(false, 'Text dispose threw error: ' + e.message);
}
});
QUnit.test('Text.dispose - Stroke Effect Disposal', function (assert) {
assert.expect(1);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var gradient = two.makeRadialGradient(
0.5,
0.5,
1,
new Two.Stop(0, 'white'),
new Two.Stop(1, 'black')
);
var text = two.makeText('World', 200, 100);
text.stroke = gradient;
text.linewidth = 2;
two.update();
// Dispose text (should dispose gradient)
try {
text.dispose();
assert.ok(true, 'Text disposes with gradient stroke without error');
} catch (e) {
assert.ok(false, 'Text dispose threw error: ' + e.message);
}
});
// ===========================
// Points Tests
// ===========================
QUnit.test(
'Points.dispose - Vertices Collection Unbinding',
function (assert) {
assert.expect(1);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var points = two.makePoints([
new Two.Vector(0, 0),
new Two.Vector(100, 50),
new Two.Vector(50, 100),
]);
// Dispose should unbind vertices collection without error
try {
points.dispose();
assert.ok(
true,
'Points disposes and unbinds vertices collection without error'
);
} catch (e) {
assert.ok(false, 'Points dispose threw error: ' + e.message);
}
}
);
QUnit.test('Points.dispose - Individual Vector Unbinding', function (assert) {
assert.expect(7);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var points = two.makePoints([
new Two.Vector(0, 0),
new Two.Vector(100, 50),
new Two.Vector(50, 100),
]);
var vector0EventFired = false;
var vector1EventFired = false;
var vector2EventFired = false;
points.vertices[0].bind('change', function () {
vector0EventFired = true;
});
points.vertices[1].bind('change', function () {
vector1EventFired = true;
});
points.vertices[2].bind('change', function () {
vector2EventFired = true;
});
// Verify events fire before dispose
points.vertices[0].trigger('change');
points.vertices[1].trigger('change');
points.vertices[2].trigger('change');
assert.ok(vector0EventFired, 'Vector 0 event fires before dispose');
assert.ok(vector1EventFired, 'Vector 1 event fires before dispose');
assert.ok(vector2EventFired, 'Vector 2 event fires before dispose');
// Reset
vector0EventFired = false;
vector1EventFired = false;
vector2EventFired = false;
// Dispose
points.dispose();
// Verify events don't fire after dispose
points.vertices[0].trigger('change');
points.vertices[1].trigger('change');
points.vertices[2].trigger('change');
assert.ok(!vector0EventFired, 'Vector 0 event does not fire after dispose');
assert.ok(!vector1EventFired, 'Vector 1 event does not fire after dispose');
assert.ok(!vector2EventFired, 'Vector 2 event does not fire after dispose');
// Verify _bound flag
assert.ok(!points.vertices[0]._bound, 'Vector 0 _bound flag is false');
});
QUnit.test('Points.dispose - Fill/Stroke Effect Disposal', function (assert) {
assert.expect(1);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var fillGradient = two.makeLinearGradient(
0,
0,
1,
1,
new Two.Stop(0, 'red'),
new Two.Stop(1, 'yellow')
);
var strokeGradient = two.makeLinearGradient(
0,
0,
1,
0,
new Two.Stop(0, 'blue'),
new Two.Stop(1, 'green')
);
var points = two.makePoints([
new Two.Vector(50, 50),
new Two.Vector(150, 50),
new Two.Vector(100, 150),
]);
points.fill = fillGradient;
points.stroke = strokeGradient;
two.update();
// Dispose points (should dispose both gradients)
try {
points.dispose();
assert.ok(
true,
'Points disposes with gradient fill/stroke without error'
);
} catch (e) {
assert.ok(false, 'Points dispose threw error: ' + e.message);
}
});
// ===========================
// Gradient Tests
// ===========================
QUnit.test('Gradient.dispose - SVG Element Removal', function (assert) {
assert.expect(3);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var gradient = two.makeLinearGradient(
0,
0,
1,
1,
new Two.Stop(0, '#ff0000'),
new Two.Stop(1, '#0000ff')
);
var rect = two.makeRectangle(100, 100, 100, 100);
rect.fill = gradient;
two.update();
// Verify gradient has DOM element
var gradientElemBefore = two.renderer.domElement.querySelector(
'#' + gradient.id
);
assert.ok(
gradientElemBefore,
'Gradient element exists in DOM before dispose'
);
assert.ok(
gradient._renderer.elem,
'Gradient has _renderer.elem before dispose'
);
// Dispose gradient directly
gradient.dispose();
// Verify gradient DOM element removed
var gradientElemAfter = two.renderer.domElement.querySelector(
'#' + gradient.id
);
assert.ok(
!gradientElemAfter,
'Gradient element removed from DOM after dispose'
);
});
QUnit.test('Gradient.dispose - Direct Disposal', function (assert) {
assert.expect(1);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var gradient = two.makeRadialGradient(
0.5,
0.5,
1,
new Two.Stop(0, 'white'),
new Two.Stop(1, 'black')
);
// Dispose gradient not attached to any shape
try {
gradient.dispose();
assert.ok(
true,
'Gradient disposes without error when not attached to shape'
);
} catch (e) {
assert.ok(false, 'Gradient dispose threw error: ' + e.message);
}
});
QUnit.test('Gradient.dispose - Event Unbinding', function (assert) {
assert.expect(1);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var gradient = two.makeLinearGradient(
0,
0,
1,
1,
new Two.Stop(0, 'red'),
new Two.Stop(1, 'blue')
);
// Dispose gradient directly
try {
gradient.dispose();
assert.ok(true, 'Gradient disposes without error');
} catch (e) {
assert.ok(false, 'Gradient dispose threw error: ' + e.message);
}
});
// ===========================
// Texture Tests
// ===========================
QUnit.test('Texture.dispose - SVG Element Removal', function (assert) {
assert.expect(4);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var texture = new Two.Texture('./images/canvas/line@2x.png');
var rect = two.makeRectangle(100, 100, 100, 100);
rect.fill = texture;
two.update();
// Wait for texture to render
var textureElemBefore = two.renderer.domElement.querySelector(
'#' + texture.id
);
var hasElemBefore = !!texture._renderer.elem;
assert.ok(
hasElemBefore || !textureElemBefore,
'Texture rendering state tracked'
);
// Dispose texture directly
texture.dispose();
// Verify texture DOM element removed
var textureElemAfter = two.renderer.domElement.querySelector(
'#' + texture.id
);
assert.ok(
!textureElemAfter,
'Texture element removed from DOM after dispose'
);
assert.ok(
!texture._renderer.elem,
'Texture _renderer.elem deleted after dispose'
);
assert.ok(texture._renderer.type, 'Texture renderer type preserved');
});
QUnit.test('Texture.dispose - Parent Dispose Called', function (assert) {
assert.expect(3);
// eslint-disable-next-line no-unused-vars
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var texture = new Two.Texture('./images/canvas/line@2x.png');
// Bind event to texture
var eventFired = false;
texture.bind('change', function () {
eventFired = true;
});
texture.trigger('change');
assert.ok(eventFired, 'Texture event fires before dispose');
eventFired = false;
// Dispose (should call Element.dispose via inheritance)
texture.dispose();
// Verify Element.dispose was called (events unbound)
texture.trigger('change');
assert.ok(
!eventFired,
'Texture event does not fire after dispose (Element.dispose called)'
);
assert.ok(!texture._bound, 'Texture _bound flag is false');
});
QUnit.test('Texture.dispose - Event Unbinding', function (assert) {
assert.expect(3);
// eslint-disable-next-line no-unused-vars
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var texture = new Two.Texture('./images/canvas/line@2x.png');
var eventFired = false;
texture.bind('change', function () {
eventFired = true;
});
// Verify event fires before dispose
texture.trigger('change');
assert.ok(eventFired, 'Texture event fires before dispose');
// Reset
eventFired = false;
// Dispose
texture.dispose();
// Verify event doesn't fire after dispose
texture.trigger('change');
assert.ok(!eventFired, 'Texture event does not fire after dispose');
assert.ok(!texture._bound, 'Texture _bound flag is false');
});
// ===========================
// Sprite Tests
// ===========================
QUnit.test('Sprite.dispose - Animation Stop', function (assert) {
assert.expect(2);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var texture = new Two.Texture('./images/canvas/line@2x.png');
var sprite = two.makeSprite(texture, 100, 100, 4, 1);
sprite.play();
assert.ok(sprite._playing, 'Sprite is playing before dispose');
// Dispose
sprite.dispose();
// Verify animation stopped
assert.ok(!sprite._playing, 'Sprite is not playing after dispose');
});
QUnit.test('Sprite.dispose - Callback Cleanup', function (assert) {
assert.expect(2);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var texture = new Two.Texture('./images/canvas/line@2x.png');
var sprite = two.makeSprite(texture, 100, 100, 4, 1);
// eslint-disable-next-line no-unused-vars
var callbackFired = false;
sprite._onLastFrame = function () {
callbackFired = true;
};
assert.ok(
sprite._onLastFrame !== null,
'Sprite has _onLastFrame callback before dispose'
);
// Dispose
sprite.dispose();
// Verify callback cleared
assert.ok(
sprite._onLastFrame === null,
'Sprite _onLastFrame callback is null after dispose'
);
});
QUnit.test('Sprite.dispose - Timing Reset', function (assert) {
assert.expect(2);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var texture = new Two.Texture('./images/canvas/line@2x.png');
var sprite = two.makeSprite(texture, 100, 100, 4, 1);
sprite.play();
// Manually set start time to simulate animation running
sprite._startTime = Date.now();
assert.ok(sprite._startTime > 0, 'Sprite has _startTime before dispose');
// Dispose
sprite.dispose();
// Verify timing reset
assert.equal(sprite._startTime, 0, 'Sprite _startTime is 0 after dispose');
});
QUnit.test('Sprite.dispose - Texture Disposal', function (assert) {
assert.expect(3);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var texture = new Two.Texture('./images/canvas/line@2x.png');
var sprite = two.makeSprite(texture, 100, 100, 4, 1);
// Bind event to texture
var textureEventFired = false;
texture.bind('change', function () {
textureEventFired = true;
});
texture.trigger('change');
assert.ok(textureEventFired, 'Texture event fires before sprite dispose');
textureEventFired = false;
// Dispose sprite (should dispose texture)
sprite.dispose();
// Verify texture disposed
texture.trigger('change');
assert.ok(
!textureEventFired,
'Texture event does not fire after sprite dispose'
);
assert.ok(!texture._bound, 'Texture _bound flag is false');
});
// ===========================
// Image Tests
// ===========================
QUnit.test('Image.dispose - Texture Disposal', function (assert) {
assert.expect(3);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var image = two.makeImage('./images/canvas/line@2x.png', 100, 100);
// Get the texture
var texture = image.texture;
// Bind event to texture
var textureEventFired = false;
texture.bind('change', function () {
textureEventFired = true;
});
texture.trigger('change');
assert.ok(textureEventFired, 'Texture event fires before image dispose');
textureEventFired = false;
// Dispose image (should dispose texture)
image.dispose();
// Verify texture disposed
texture.trigger('change');
assert.ok(
!textureEventFired,
'Texture event does not fire after image dispose'
);
assert.ok(!texture._bound, 'Texture _bound flag is false');
});
QUnit.test('Image.dispose - Inheritance Chain', function (assert) {
assert.expect(4);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var image = two.makeImage('./images/canvas/line@2x.png', 100, 100);
// Bind event to image
var eventFired = false;
image.bind('change', function () {
eventFired = true;
});
image.trigger('change');
assert.ok(eventFired, 'Image event fires before dispose');
eventFired = false;
two.update();
// Dispose image
image.dispose();
// Verify Element/Shape/Path dispose called (events unbound)
image.trigger('change');
assert.ok(!eventFired, 'Image event does not fire after dispose');
// Verify Rectangle/Path dispose called (vertices unbound)
assert.ok(
!image.vertices._bound,
'Image vertices collection _bound flag is false'
);
// Verify DOM cleanup
var imageElemAfter = two.renderer.domElement.querySelector('#' + image.id);
assert.ok(!imageElemAfter, 'Image DOM element removed');
});
// ===========================
// ImageSequence Tests
// ===========================
QUnit.test('ImageSequence.dispose - Animation Stop', function (assert) {
assert.expect(2);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var sequence = two.makeImageSequence(
['./images/canvas/line@2x.png', './images/canvas/line@2x.png'],
100,
100
);
sequence.play();
assert.ok(sequence._playing, 'ImageSequence is playing before dispose');
// Dispose
sequence.dispose();
// Verify animation stopped
assert.ok(!sequence._playing, 'ImageSequence is not playing after dispose');
});
QUnit.test(
'ImageSequence.dispose - Textures Collection Unbinding',
function (assert) {
assert.expect(1);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var sequence = two.makeImageSequence(
['./images/canvas/line@2x.png', './images/canvas/line@2x.png'],
100,
100
);
// Dispose should unbind textures collection without error
try {
sequence.dispose();
assert.ok(
true,
'ImageSequence disposes and unbinds textures collection without error'
);
} catch (e) {
assert.ok(false, 'ImageSequence dispose threw error: ' + e.message);
}
}
);
QUnit.test(
'ImageSequence.dispose - Individual Texture Disposal',
function (assert) {
assert.expect(5);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var sequence = two.makeImageSequence(
['./images/canvas/line@2x.png', './images/canvas/line@2x.png'],
100,
100
);
var texture0EventFired = false;
var texture1EventFired = false;
sequence.textures[0].bind('change', function () {
texture0EventFired = true;
});
sequence.textures[1].bind('change', function () {
texture1EventFired = true;
});
// Verify events fire before dispose
sequence.textures[0].trigger('change');
sequence.textures[1].trigger('change');
assert.ok(texture0EventFired, 'Texture 0 event fires before dispose');
assert.ok(texture1EventFired, 'Texture 1 event fires before dispose');
// Reset
texture0EventFired = false;
texture1EventFired = false;
// Dispose sequence (should dispose all textures)
sequence.dispose();
// Verify textures disposed
sequence.textures[0].trigger('change');
sequence.textures[1].trigger('change');
assert.ok(
!texture0EventFired,
'Texture 0 event does not fire after sequence dispose'
);
assert.ok(
!texture1EventFired,
'Texture 1 event does not fire after sequence dispose'
);
assert.ok(!sequence.textures[0]._bound, 'Texture 0 _bound flag is false');
}
);
QUnit.test(
'ImageSequence.dispose - Incomplete Collection',
function (assert) {
assert.expect(1);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400,
}).appendTo(document.body);
var sequence = two.makeImageSequence(
['./images/canvas/line@2x.png', './images/canvas/line@2x.png'],
100,
100
);
// Simulate incomplete collection by removing unbind method
var originalUnbind = sequence.textures.unbind;
delete sequence.textures.unbind;
// Dispose should handle incomplete collection gracefully
try {
sequence.dispose();
assert.ok(
true,
'ImageSequence disposes without error when textures collection has no unbind method'
);
} catch (e) {
assert.ok(
false,
'ImageSequence dispose threw error with incomplete collection: ' +
e.message
);
}
// Restore for cleanup
sequence.textures.unbind = originalUnbind;
}
);
})();
================================================
FILE: tests/suite/hit-test.js
================================================
QUnit.module('Hit Testing');
QUnit.test('Shape.contains evaluates fill and stroke geometry', function (assert) {
assert.expect(8);
var two = new Two({ width: 400, height: 400, autostart: false });
var circle = new Two.Circle(100, 100, 20);
two.add(circle);
two.update();
assert.ok(circle.contains(100, 100), 'Circle contains its center point.');
assert.notOk(circle.contains(140, 100), 'Circle excludes distant point.');
var line = new Two.Line(0, 0, 100, 0);
line.linewidth = 12;
two.add(line);
two.update();
assert.ok(line.contains(50, 4), 'Line stroke hit succeeds within tolerance.');
assert.notOk(line.contains(50, 40), 'Line stroke rejects far point.');
var rect = new Two.Rectangle(150, 150, 40, 40);
rect.rotation = Math.PI / 4;
two.add(rect);
two.update();
assert.ok(rect.contains(150, 150), 'Rotated rectangle contains its center.');
assert.notOk(rect.contains(200, 150), 'Rotated rectangle excludes outside point.');
var invisible = new Two.Circle(200, 200, 10);
invisible.visible = false;
two.add(invisible);
two.update();
assert.notOk(
invisible.contains(200, 200),
'Invisible shapes bypass contains unless ignoreVisibility is set.'
);
assert.ok(
invisible.contains(200, 200, { ignoreVisibility: true }),
'ignoreVisibility option bypasses visibility check.'
);
});
QUnit.test('Two#getShapesAtPoint respects options', function (assert) {
assert.expect(6);
var two = new Two({ width: 400, height: 400, autostart: false });
var circle = new Two.Circle(100, 100, 20);
two.add(circle);
var line = new Two.Line(0, 0, 100, 0);
line.linewidth = 12;
two.add(line);
var rect = new Two.Rectangle(150, 150, 40, 40);
rect.rotation = Math.PI / 4;
two.add(rect);
var hidden = new Two.Circle(200, 200, 10);
hidden.visible = false;
two.add(hidden);
two.update();
var circleHits = two.getShapesAtPoint(100, 100);
assert.ok(circleHits.indexOf(circle) > -1, 'Circle reported at its center.');
var sceneHits = two.scene.getShapesAtPoint(100, 100);
assert.ok(
sceneHits.indexOf(circle) > -1,
'Scene group reports circle at its center.'
);
var deepest = two.getShapesAtPoint(50, 4, { mode: 'deepest' });
assert.deepEqual(deepest, [line], 'Deepest mode returns top-most shape only.');
var visibleHits = two.getShapesAtPoint(200, 200);
assert.strictEqual(
visibleHits.indexOf(hidden),
-1,
'Hidden shapes excluded when visibleOnly is true.'
);
var allHits = two.getShapesAtPoint(200, 200, { visibleOnly: false });
assert.ok(
allHits.indexOf(hidden) > -1,
'Hidden shapes included when visibleOnly is false.'
);
var sceneAll = two.scene.getShapesAtPoint(200, 200, {
visibleOnly: false,
});
assert.ok(
sceneAll.indexOf(hidden) > -1,
'Scene group honours visibleOnly option.'
);
});
================================================
FILE: tests/suite/release.js
================================================
/**
* Tests Two.js release() Method Enhanced Functionality:
* + Basic release functionality
* + SVG DOM element cleanup
* + Canvas renderer cleanup
* + WebGL resource cleanup
* + Memory leak prevention
* + Recursive cleanup of nested objects
*/
(function () {
QUnit.module('Release Method');
QUnit.test('Two.release - Basic Functionality', function (assert) {
assert.expect(3);
var two = new Two({
width: 400,
height: 400
});
var rect = two.makeRectangle(100, 100, 50, 50);
// Test release method exists
assert.ok(typeof two.release === 'function', 'Two.release method exists');
// Test release returns the object
var result = two.release(rect);
assert.equal(result, rect, 'Two.release returns the released object');
// Test release works with undefined (the default case)
var undefinedResult = two.release();
assert.ok(undefinedResult !== null, 'Two.release handles undefined gracefully by releasing scene');
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.release - SVG DOM Element Cleanup', function (assert) {
assert.expect(6);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400
}).appendTo(document.body);
var rect = two.makeRectangle(100, 100, 50, 50);
var circle = two.makeCircle(200, 200, 25);
two.update();
// Verify elements exist in DOM
var rectElem = two.renderer.domElement.querySelector('#' + rect.id);
var circleElem = two.renderer.domElement.querySelector('#' + circle.id);
assert.ok(rectElem, 'Rectangle SVG element exists in DOM before release');
assert.ok(circleElem, 'Circle SVG element exists in DOM before release');
assert.ok(rect._renderer.elem, 'Rectangle has _renderer.elem before release');
assert.ok(circle._renderer.elem, 'Circle has _renderer.elem before release');
// Release objects
two.release(rect);
two.release(circle);
// Verify elements are removed from DOM
var rectElemAfter = two.renderer.domElement.querySelector('#' + rect.id);
var circleElemAfter = two.renderer.domElement.querySelector('#' + circle.id);
assert.ok(!rectElemAfter, 'Rectangle SVG element removed from DOM after release');
assert.ok(!circleElemAfter, 'Circle SVG element removed from DOM after release');
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.release - SVG Gradient Cleanup', function (assert) {
assert.expect(4);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400
}).appendTo(document.body);
var gradient = two.makeLinearGradient(0, 0, 1, 1,
new Two.Stop(0, 'red'),
new Two.Stop(1, 'blue')
);
var rect = two.makeRectangle(100, 100, 50, 50);
rect.fill = gradient;
two.update();
// Verify gradient exists in DOM
var gradientElem = two.renderer.domElement.querySelector('#' + gradient.id);
assert.ok(gradientElem, 'Gradient SVG element exists in DOM before release');
assert.ok(gradient._renderer.elem, 'Gradient has _renderer.elem before release');
// Release gradient
two.release(gradient);
// Verify gradient is removed from DOM
var gradientElemAfter = two.renderer.domElement.querySelector('#' + gradient.id);
assert.ok(!gradientElemAfter, 'Gradient SVG element removed from DOM after release');
assert.ok(!gradient._renderer.elem, 'Gradient _renderer.elem is cleared after release');
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.release - Canvas Renderer Cleanup', function (assert) {
assert.expect(3);
var two = new Two({
type: Two.Types.canvas,
width: 400,
height: 400
}).appendTo(document.body);
var rect = two.makeRectangle(100, 100, 50, 50);
two.update();
// Verify renderer context exists
assert.ok(two.renderer.ctx, 'Canvas renderer has context');
assert.ok(rect._renderer, 'Rectangle has _renderer object');
// Release object
two.release(rect);
// Verify cleanup (context references should be cleared if they existed)
// Canvas cleanup mainly involves clearing cached references
assert.ok(true, 'Canvas release completed without errors');
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.release - WebGL Resource Cleanup', function (assert) {
assert.expect(5);
var two = new Two({
type: Two.Types.webgl,
width: 400,
height: 400
}).appendTo(document.body);
var rect = two.makeRectangle(100, 100, 50, 50);
two.update();
// Verify WebGL context and basic setup
var gl = two.renderer.ctx;
assert.ok(gl, 'WebGL context exists');
assert.ok(rect._renderer, 'Rectangle has _renderer object');
// Test that we can release WebGL objects without errors
var releaseResult = two.release(rect);
assert.equal(releaseResult, rect, 'WebGL object release returns the object');
// Test effect cleanup - manually set an effect and verify it gets cleared
var rect2 = two.makeRectangle(150, 150, 50, 50);
rect2._renderer.effect = 'test-effect';
two.release(rect2);
assert.ok(!rect2._renderer.effect, 'WebGL effect cleared after release');
assert.ok(true, 'WebGL release completed without errors');
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.release - Recursive Group Cleanup', function (assert) {
assert.expect(8);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400
}).appendTo(document.body);
var group = two.makeGroup();
var rect1 = two.makeRectangle(100, 100, 50, 50);
var rect2 = two.makeRectangle(200, 200, 50, 50);
var subGroup = two.makeGroup();
var circle = two.makeCircle(150, 150, 25);
// Build hierarchy
group.add(rect1, rect2, subGroup);
subGroup.add(circle);
two.update();
// Verify all elements exist in DOM
assert.ok(two.renderer.domElement.querySelector('#' + group.id), 'Group exists in DOM');
assert.ok(two.renderer.domElement.querySelector('#' + rect1.id), 'Rect1 exists in DOM');
assert.ok(two.renderer.domElement.querySelector('#' + rect2.id), 'Rect2 exists in DOM');
assert.ok(two.renderer.domElement.querySelector('#' + subGroup.id), 'SubGroup exists in DOM');
assert.ok(two.renderer.domElement.querySelector('#' + circle.id), 'Circle exists in DOM');
// Release the group (should recursively clean up children)
two.release(group);
// Verify all elements are removed from DOM
assert.ok(!two.renderer.domElement.querySelector('#' + rect1.id), 'Rect1 removed from DOM');
assert.ok(!two.renderer.domElement.querySelector('#' + rect2.id), 'Rect2 removed from DOM');
assert.ok(!two.renderer.domElement.querySelector('#' + circle.id), 'Circle removed from DOM');
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.release - Event Unbinding', function (assert) {
assert.expect(4);
var two = new Two({
width: 400,
height: 400
});
var rect = two.makeRectangle(100, 100, 50, 50);
var eventFired = false;
// Bind event listener
rect.bind('change', function() {
eventFired = true;
});
// Test event fires before release
rect.trigger('change');
assert.ok(eventFired, 'Event fires before release');
// Reset flag
eventFired = false;
// Release object
two.release(rect);
// Test event doesn't fire after release
rect.trigger('change');
assert.ok(!eventFired, 'Event does not fire after release');
// Test vertex event unbinding
var path = two.makePath(0, 0, 100, 0, 100, 100, 0, 100);
var vertexEventFired = false;
if (path.vertices && path.vertices[0]) {
path.vertices[0].bind('change', function() {
vertexEventFired = true;
});
path.vertices[0].trigger('change');
assert.ok(vertexEventFired, 'Vertex event fires before release');
vertexEventFired = false;
two.release(path);
path.vertices[0].trigger('change');
assert.ok(!vertexEventFired, 'Vertex event does not fire after release');
} else {
assert.ok(true, 'Path has no vertices to test');
assert.ok(true, 'Skipping vertex event test');
}
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.release - Effects Cleanup', function (assert) {
assert.expect(6);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400
}).appendTo(document.body);
var gradient = two.makeLinearGradient(0, 0, 1, 1,
new Two.Stop(0, 'red'),
new Two.Stop(1, 'blue')
);
var rect = two.makeRectangle(100, 100, 50, 50);
rect.fill = gradient;
rect.stroke = gradient;
var eventFired = false;
gradient.bind('change', function() {
eventFired = true;
});
two.update();
// Test effects are bound
gradient.trigger('change');
assert.ok(eventFired, 'Gradient event fires before release');
eventFired = false;
// Release object (should clean up effects)
two.release(rect);
// Test that gradient events are unbound for fill
if (rect.fill && typeof rect.fill.unbind === 'function') {
gradient.trigger('change');
assert.ok(!eventFired, 'Fill gradient event does not fire after release');
} else {
assert.ok(true, 'Fill gradient already unbound or not applicable');
}
// Test that gradient events are unbound for stroke
if (rect.stroke && typeof rect.stroke.unbind === 'function') {
gradient.trigger('change');
assert.ok(!eventFired, 'Stroke gradient event does not fire after release');
} else {
assert.ok(true, 'Stroke gradient already unbound or not applicable');
}
// Test gradient disposal
var gradientElem = two.renderer.domElement.querySelector('#' + gradient.id);
if (gradientElem) {
assert.ok(gradientElem, 'Gradient still exists in DOM (expected for shared effects)');
} else {
assert.ok(true, 'Gradient removed from DOM');
}
// Release the gradient itself
two.release(gradient);
var gradientElemAfter = two.renderer.domElement.querySelector('#' + gradient.id);
assert.ok(!gradientElemAfter, 'Gradient removed from DOM after direct release');
assert.ok(true, 'Effects cleanup completed without errors');
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.release - Scene Release', function (assert) {
assert.expect(4);
var two = new Two({
type: Two.Types.svg,
width: 400,
height: 400
}).appendTo(document.body);
var rect = two.makeRectangle(100, 100, 50, 50);
var circle = two.makeCircle(200, 200, 25);
two.update();
// Verify elements exist
assert.ok(two.renderer.domElement.querySelector('#' + rect.id), 'Rectangle exists before scene release');
assert.ok(two.renderer.domElement.querySelector('#' + circle.id), 'Circle exists before scene release');
// Release entire scene
two.release();
// Verify all elements are cleaned up
assert.ok(!two.renderer.domElement.querySelector('#' + rect.id), 'Rectangle removed after scene release');
assert.ok(!two.renderer.domElement.querySelector('#' + circle.id), 'Circle removed after scene release');
QUnit.Utils.addInstanceToTest(assert.test, two);
});
})();
================================================
FILE: tests/suite/shapes.js
================================================
QUnit.module('Primitives');
QUnit.test('Two.Points', function (assert) {
var props = Two.Points.Properties.slice(0, 7);
assert.expect(props.length + 15);
var two = new Two();
var points = new Two.Points();
two.add(points);
two.update();
points = new Two.Points([
new Two.Anchor(0, 0),
new Two.Anchor(100, 100),
new Two.Anchor(200, 200),
new Two.Anchor(300, 300),
]);
assert.equal(points.vertices.length, 4, 'Amount of vertices set correctly.');
points.fill = 'red';
assert.equal(points.fill, 'red', 'Can get property fill correctly.');
assert.equal(points._fill, 'red', 'Can set property fill correctly.');
points.stroke = 'red';
assert.equal(points.stroke, 'red', 'Can get property stroke correctly.');
assert.equal(points._stroke, 'red', 'Can set property stroke correctly.');
points.size = 6;
assert.equal(points.size, 6, 'Can get property size correctly.');
assert.equal(points._size, 6, 'Can set property size correctly.');
points.sizeAttenuation = true;
assert.equal(
points.sizeAttenuation,
true,
'Can get property sizeAttenuation correctly.'
);
assert.equal(
points._sizeAttenuation,
true,
'Can set property sizeAttenuation correctly.'
);
points.strokeAttenuation = true;
assert.equal(
points.strokeAttenuation,
true,
'Can get property strokeAttenuation correctly.'
);
assert.equal(
points._strokeAttenuation,
true,
'Can set property strokeAttenuation correctly.'
);
points.dashes = [2, 2];
assert.equal(
points.dashes.length === 2 &&
points.dashes[0] === 2 &&
points.dashes[1] === 2,
true,
'Can get property dashes correctly.'
);
assert.equal(
points._dashes.length === 2 &&
points._dashes[0] === 2 &&
points._dashes[1] === 2,
true,
'Can set property dashes correctly.'
);
points.className = 'ui-points';
assert.equal(
points.className,
'ui-points',
'Can get property className correctly.'
);
assert.equal(
points._className,
'ui-points',
'Can set property className correctly.'
);
points._update();
points.flagReset();
for (var i = 0; i < props.length; i++) {
var prop = props[i];
assert.equal(
points['_flag' + prop.charAt(0).toUpperCase() + prop.slice(1)],
false,
'Reset flag ' + prop + ' correctly.'
);
}
points.beginning = 0.5;
points.ending = 0.5;
two.add(points);
two.update();
});
QUnit.test('Two.Points Object Conversion', function (assert) {
assert.expect(7);
var point = new Two.Points(new Two.Anchor(50, 50));
point.id = 'my-point';
var obj = point.toObject();
assert.equal(typeof obj, 'object', 'Two.Points.toObject creates an object');
assert.equal(obj.vertices[0].x, 50, 'Two.Points.toObject preserves x');
assert.equal(obj.vertices[0].y, 50, 'Two.Points.toObject preserves y');
assert.equal(obj.id, 'my-point', 'Two.Points.toObject preserves id');
var newPoint = Two.Points.fromObject(obj);
assert.equal(
newPoint.vertices[0].x,
point.vertices[0].x,
'Two.Points.fromObject preserves x'
);
assert.equal(
newPoint.vertices[0].y,
point.vertices[0].y,
'Two.Points.fromObject preserves y'
);
var copiedPoint = new Two.Points().copy(point);
assert.deepEqual(
{ ...copiedPoint.toObject(), id: point.id },
point.toObject(),
'Two.Points.copy creates identical point'
);
});
QUnit.test('Two.ArcSegment', function (assert) {
assert.expect(Two.ArcSegment.Properties.length * 4 + 2);
var innerRadius = 5;
var outerRadius = 10;
var startAngle = 0;
var endAngle = Math.PI / 2;
var properties = [startAngle, endAngle, innerRadius, outerRadius];
var path = new Two.ArcSegment(
0,
0,
innerRadius,
outerRadius,
startAngle,
endAngle
);
assert.equal(
path.vertices.length,
Two.Resolution * 3,
'Amount of vertices set correctly.'
);
for (var i = 0; i < Two.ArcSegment.Properties.length; i++) {
var prop = Two.ArcSegment.Properties[i];
assert.equal(
path[prop],
properties[i],
'Can get property ' + prop + ' correctly.'
);
path[prop] = properties[i] * 2;
assert.equal(
path[prop],
properties[i] * 2,
'Can set property ' + prop + ' correctly.'
);
assert.equal(
path['_flag' + prop.charAt(0).toUpperCase() + prop.slice(1)],
true,
'Set ' + prop + "'s property flag correctly."
);
}
path._update();
path.flagReset();
for (var i = 0; i < Two.ArcSegment.Properties.length; i++) {
var prop = Two.ArcSegment.Properties[i];
assert.equal(
path['_flag' + prop.charAt(0).toUpperCase() + prop.slice(1)],
false,
'Reset flag ' + prop + ' correctly.'
);
}
path.closed = false;
path._update();
assert.equal(
path.vertices[0].equals(path.vertices[path.vertices.length - 1]),
true,
'Circle has standardized vertex generation when shape is not closed'
);
});
QUnit.test('Two.Path.smooth assigns handles', function (assert) {
assert.expect(5);
var a0 = new Two.Anchor(0, 0);
var a1 = new Two.Anchor(50, 100);
var a2 = new Two.Anchor(100, 0);
var path = new Two.Path([a0, a1, a2]);
assert.strictEqual(
path.automatic,
true,
'automatic defaults to true before smoothing'
);
path.smooth({ type: 'continuous' });
assert.strictEqual(
path.automatic,
false,
'smooth disables automatic plotting for manual control'
);
var middle = path.vertices[1];
assert.ok(
middle.controls.left.length() > 0,
'left handle assigned on interior vertex'
);
assert.ok(
middle.controls.right.length() > 0,
'right handle assigned on interior vertex'
);
assert.strictEqual(
middle.command,
Two.Commands.curve,
'middle vertex command converted to curve'
);
});
QUnit.test('Two.Path.subdivide recomputes handles', function (assert) {
assert.expect(5);
var start = new Two.Anchor(0, 0);
start.controls.right.set(50, 0);
var end = new Two.Anchor(100, 0);
end.controls.left.set(-50, 0);
var path = new Two.Path([start, end]);
path.subdivide(1);
assert.strictEqual(path.vertices.length, 3, 'midpoint inserted');
assert.ok(
path.vertices[0].controls.right.length() > 0,
'original start handle preserved after subdivision'
);
assert.ok(
path.vertices[2].controls.left.length() > 0,
'original end handle preserved after subdivision'
);
var mid = path.vertices[1];
assert.strictEqual(
mid.command,
Two.Commands.curve,
'midpoint inherits curve command'
);
assert.ok(
mid.controls.left.length() > 0 && mid.controls.right.length() > 0,
'midpoint handles computed from original curve'
);
});
QUnit.test('Two.ArcSegment Object Conversion', function (assert) {
assert.expect(11);
// Create original arc segment
var innerRadius = 5;
var outerRadius = 10;
var startAngle = 0;
var endAngle = Math.PI / 2;
var arcSegment = new Two.ArcSegment(
0,
0,
innerRadius,
outerRadius,
startAngle,
endAngle
);
arcSegment.fill = '#0000ff';
arcSegment.stroke = '#ff00ff';
arcSegment.linewidth = 3;
arcSegment.id = 'my-arcSegment';
// Convert to object
var obj = arcSegment.toObject();
// Test object has expected properties
assert.equal(
typeof obj,
'object',
'Two.ArcSegment.toObject creates an object'
);
assert.equal(
obj.innerRadius,
innerRadius,
'Two.ArcSegment.toObject preserves innerRadius'
);
assert.equal(
obj.outerRadius,
outerRadius,
'Two.ArcSegment.toObject preserves outerRadius'
);
assert.equal(
obj.startAngle,
startAngle,
'Two.ArcSegment.toObject preserves startAngle'
);
assert.equal(
obj.endAngle,
endAngle,
'Two.ArcSegment.toObject preserves endAngle'
);
assert.equal(obj.fill, '#0000ff', 'Two.ArcSegment.toObject preserves fill');
assert.equal(obj.id, 'my-arcSegment', 'Two.ArcSegment.toObject preserves id');
// Create new arc segment from object
var newArcSegment = Two.ArcSegment.fromObject(obj);
// Test new circle matches original
assert.equal(
newArcSegment.radius,
arcSegment.radius,
'Two.ArcSegment.fromObject preserves radius'
);
assert.equal(
newArcSegment.fill,
arcSegment.fill,
'Two.ArcSegment.fromObject preserves fill'
);
assert.equal(
newArcSegment.id,
arcSegment.id,
'Two.ArcSegment.fromObject preserves id'
);
// Test copy method
var copiedArcSegment = new Two.ArcSegment().copy(arcSegment);
assert.deepEqual(
{ ...copiedArcSegment.toObject(), id: arcSegment.id },
arcSegment.toObject(),
'Two.ArcSegment.copy creates identical arcSegment'
);
});
QUnit.test('Two.Circle', function (assert) {
assert.expect(Two.Circle.Properties.length * 4 + 2);
var radius = 50;
var properties = [radius];
var path = new Two.Circle(0, 0, radius);
assert.equal(path.vertices.length, 4, 'Amount of vertices set correctly.');
for (var i = 0; i < Two.Circle.Properties.length; i++) {
var prop = Two.Circle.Properties[i];
assert.equal(
path[prop],
properties[i],
'Can get property ' + prop + ' correctly.'
);
path[prop] = properties[i] * 2;
assert.equal(
path[prop],
properties[i] * 2,
'Can set property ' + prop + ' correctly.'
);
assert.equal(
path['_flag' + prop.charAt(0).toUpperCase() + prop.slice(1)],
true,
'Set ' + prop + "'s property flag correctly."
);
}
path._update();
path.flagReset();
for (var i = 0; i < Two.Circle.Properties.length; i++) {
var prop = Two.Circle.Properties[i];
assert.equal(
path['_flag' + prop.charAt(0).toUpperCase() + prop.slice(1)],
false,
'Reset flag ' + prop + ' correctly.'
);
}
path.closed = false;
path._update();
assert.equal(
path.vertices[0].equals(path.vertices[path.vertices.length - 1]),
true,
'Circle has standardized vertex generation when shape is not closed'
);
});
QUnit.test('Two.Circle Object Conversion', function (assert) {
assert.expect(8);
// Create original circle
var circle = new Two.Circle(100, 100, 50);
circle.fill = '#0000ff';
circle.stroke = '#ff00ff';
circle.linewidth = 3;
circle.id = 'my-circle';
// Convert to object
var obj = circle.toObject();
// Test object has expected properties
assert.equal(typeof obj, 'object', 'Two.Circle.toObject creates an object');
assert.equal(obj.radius, 50, 'Two.Circle.toObject preserves radius');
assert.equal(obj.fill, '#0000ff', 'Two.Circle.toObject preserves fill');
assert.equal(obj.id, 'my-circle', 'Two.Circle.toObject preserves id');
// Create new circle from object
var newCircle = Two.Circle.fromObject(obj);
// Test new circle matches original
assert.equal(
newCircle.radius,
circle.radius,
'Two.Circle.fromObject preserves radius'
);
assert.equal(
newCircle.fill,
circle.fill,
'Two.Circle.fromObject preserves fill'
);
assert.equal(newCircle.id, circle.id, 'Two.Circle.fromObject preserves id');
// Test copy method
var copiedCircle = new Two.Circle().copy(circle);
assert.deepEqual(
{ ...copiedCircle.toObject(), id: circle.id },
circle.toObject(),
'Two.Circle.copy creates identical circle'
);
});
QUnit.test('Two.Ellipse', function (assert) {
assert.expect(Two.Ellipse.Properties.length * 4 + 2);
var rx = 50;
var ry = 75;
var properties = [rx * 2, ry * 2];
var path = new Two.Ellipse(0, 0, rx, ry);
assert.equal(path.vertices.length, 4, 'Amount of vertices set correctly.');
for (var i = 0; i < Two.Ellipse.Properties.length; i++) {
var prop = Two.Ellipse.Properties[i];
assert.equal(
path[prop],
properties[i],
'Can get property ' + prop + ' correctly.'
);
path[prop] = properties[i] * 2;
assert.equal(
path[prop],
properties[i] * 2,
'Can set property ' + prop + ' correctly.'
);
assert.equal(
path['_flag' + prop.charAt(0).toUpperCase() + prop.slice(1)],
true,
'Set ' + prop + "'s property flag correctly."
);
}
path._update();
path.flagReset();
for (var i = 0; i < Two.Ellipse.Properties.length; i++) {
var prop = Two.Ellipse.Properties[i];
assert.equal(
path['_flag' + prop.charAt(0).toUpperCase() + prop.slice(1)],
false,
'Reset flag ' + prop + ' correctly.'
);
}
path.closed = false;
path._update();
assert.equal(
path.vertices[0].equals(path.vertices[path.vertices.length - 1]),
true,
'Ellipse has standardized vertex generation when shape is not closed'
);
});
QUnit.test('Two.Ellipse Object Conversion', function (assert) {
assert.expect(9);
// Create original ellipse
var ellipse = new Two.Ellipse(100, 100, 50, 75);
ellipse.fill = '#0000ff';
ellipse.stroke = '#ff00ff';
ellipse.linewidth = 3;
ellipse.id = 'my-ellipse';
// Convert to object
var obj = ellipse.toObject();
// Test object has expected properties
assert.equal(typeof obj, 'object', 'Two.Ellipse.toObject creates an object');
assert.equal(obj.width, 100, 'Two.Ellipse.toObject preserves width');
assert.equal(obj.height, 150, 'Two.Ellipse.toObject preserves height');
assert.equal(obj.fill, '#0000ff', 'Two.Ellipse.toObject preserves fill');
assert.equal(obj.id, 'my-ellipse', 'Two.Ellipse.toObject preserves id');
// Create new ellipse from object
var newEllipse = Two.Ellipse.fromObject(obj);
// Test new ellipse matches original
assert.equal(
newEllipse.width,
ellipse.width,
'Two.Ellipse.fromObject preserves width'
);
assert.equal(
newEllipse.height,
ellipse.height,
'Two.Ellipse.fromObject preserves height'
);
assert.equal(
newEllipse.fill,
ellipse.fill,
'Two.Ellipse.fromObject preserves fill'
);
// Test copy method
var copiedEllipse = new Two.Ellipse().copy(ellipse);
assert.deepEqual(
{ ...copiedEllipse.toObject(), id: ellipse.id },
ellipse.toObject(),
'Two.Ellipse.copy creates identical ellipse'
);
});
QUnit.test('Two.Polygon', function (assert) {
assert.expect(Two.Polygon.Properties.length * 4 + 1);
var radius = 50;
var sides = 5;
var properties = [radius * 2, radius * 2, sides];
var path = new Two.Polygon(0, 0, radius, sides);
for (var i = 0; i < Two.Polygon.Properties.length; i++) {
var prop = Two.Polygon.Properties[i];
assert.equal(
path[prop],
properties[i],
'Can get property ' + prop + ' correctly.'
);
path[prop] = properties[i] * 2;
assert.equal(
path[prop],
properties[i] * 2,
'Can set property ' + prop + ' correctly.'
);
assert.equal(
path['_flag' + prop.charAt(0).toUpperCase() + prop.slice(1)],
true,
'Set ' + prop + "'s property flag correctly."
);
}
path._update();
path.flagReset();
for (var i = 0; i < Two.Polygon.Properties.length; i++) {
var prop = Two.Polygon.Properties[i];
assert.equal(
path['_flag' + prop.charAt(0).toUpperCase() + prop.slice(1)],
false,
'Reset flag ' + prop + ' correctly.'
);
}
path.closed = false;
path._update();
assert.equal(
path.vertices[0].equals(path.vertices[path.vertices.length - 1]),
true,
'Polygon has standardized vertex generation when shape is not closed'
);
});
QUnit.test('Two.Polygon Object Conversion', function (assert) {
assert.expect(9);
// Create original polygon
var polygon = new Two.Polygon(100, 100, 50, 5);
polygon.fill = '#0000ff';
polygon.stroke = '#ff00ff';
polygon.linewidth = 3;
polygon.id = 'my-polygon';
// Convert to object
var obj = polygon.toObject();
// Test object has expected properties
assert.equal(typeof obj, 'object', 'Two.Polygon.toObject creates an object');
assert.equal(obj.width, 100, 'Two.Polygon.toObject preserves width');
assert.equal(obj.height, 100, 'Two.Polygon.toObject preserves height');
assert.equal(obj.sides, 5, 'Two.Polygon.toObject preserves sides');
assert.equal(obj.fill, '#0000ff', 'Two.Polygon.toObject preserves fill');
assert.equal(obj.id, 'my-polygon', 'Two.Polygon.toObject preserves id');
// Create new polygon from object
var newPolygon = Two.Polygon.fromObject(obj);
// Test new polygon matches original
assert.equal(
newPolygon.width,
polygon.width,
'Two.Polygon.fromObject preserves width'
);
assert.equal(
newPolygon.sides,
polygon.sides,
'Two.Polygon.fromObject preserves sides'
);
// Test copy method
var copiedPolygon = new Two.Polygon().copy(polygon);
assert.deepEqual(
{ ...copiedPolygon.toObject(), id: polygon.id },
polygon.toObject(),
'Two.Polygon.copy creates identical polygon'
);
});
QUnit.test('Two.Rectangle', function (assert) {
assert.expect(Two.Rectangle.Properties.length * 4 + 1);
var width = 50;
var height = 75;
var origin = '{"x":0,"y":0}';
var properties = [width, height, origin];
var path = new Two.Rectangle(0, 0, width, height);
for (var i = 0; i < Two.Rectangle.Properties.length; i++) {
var prop = Two.Rectangle.Properties[i];
var isOrigin = i === 2;
assert.equal(
isOrigin ? JSON.stringify(path[prop].toObject()) : path[prop],
properties[i],
'Can get property ' + prop + ' correctly.'
);
if (isOrigin) {
path[prop].set(50, 50);
assert.equal(
JSON.stringify(path[prop].toObject()),
'{"x":50,"y":50}',
'Can set property ' + prop + ' correctly.'
);
} else {
path[prop] = properties[i] * 2;
assert.equal(
path[prop],
properties[i] * 2,
'Can set property ' + prop + ' correctly.'
);
}
if (isOrigin) {
console.log(path._flagVertices);
assert.equal(
path['_flagVertices'],
true,
'Set ' + prop + "'s property flag correctly."
);
} else {
assert.equal(
path['_flag' + prop.charAt(0).toUpperCase() + prop.slice(1)],
true,
'Set ' + prop + "'s property flag correctly."
);
}
}
path._update();
path.flagReset();
for (var i = 0; i < Two.Rectangle.Properties.length; i++) {
var prop = Two.Rectangle.Properties[i];
var isOrigin = i === 2;
if (isOrigin) {
assert.equal(
path['_flagVertices'],
false,
'Reset flag ' + prop + ' correctly.'
);
} else {
assert.equal(
path['_flag' + prop.charAt(0).toUpperCase() + prop.slice(1)],
false,
'Reset flag ' + prop + ' correctly.'
);
}
}
path.closed = false;
path._update();
assert.equal(
path.vertices[0].equals(path.vertices[path.vertices.length - 1]),
true,
'Rectangle has standardized vertex generation when shape is not closed'
);
});
QUnit.test('Two.Rectangle Object Conversion', function (assert) {
assert.expect(9);
// Create original rectangle
var rectangle = new Two.Rectangle(100, 100, 50, 75);
rectangle.fill = '#0000ff';
rectangle.stroke = '#ff00ff';
rectangle.linewidth = 3;
rectangle.id = 'my-rectangle';
// Convert to object
var obj = rectangle.toObject();
// Test object has expected properties
assert.equal(
typeof obj,
'object',
'Two.Rectangle.toObject creates an object'
);
assert.equal(obj.width, 50, 'Two.Rectangle.toObject preserves width');
assert.equal(obj.height, 75, 'Two.Rectangle.toObject preserves height');
assert.equal(obj.fill, '#0000ff', 'Two.Rectangle.toObject preserves fill');
assert.equal(obj.id, 'my-rectangle', 'Two.Rectangle.toObject preserves id');
// Create new rectangle from object
var newRectangle = Two.Rectangle.fromObject(obj);
// Test new rectangle matches original
assert.equal(
newRectangle.width,
rectangle.width,
'Two.Rectangle.fromObject preserves width'
);
assert.equal(
newRectangle.height,
rectangle.height,
'Two.Rectangle.fromObject preserves height'
);
assert.equal(
newRectangle.fill,
rectangle.fill,
'Two.Rectangle.fromObject preserves fill'
);
// Test copy method
var copiedRectangle = new Two.Rectangle().copy(rectangle);
copiedRectangle._update();
assert.deepEqual(
{ ...copiedRectangle.toObject(), id: rectangle.id },
rectangle.toObject(),
'Two.Rectangle.copy creates identical rectangle'
);
});
QUnit.test('Two.RoundedRectangle', function (assert) {
assert.expect(Two.RoundedRectangle.Properties.length * 4 + 3);
var width = 50;
var height = 75;
var radius = 8;
var properties = [width, height, radius];
var path = new Two.RoundedRectangle(0, 0, width, height, radius);
for (var i = 0; i < Two.RoundedRectangle.Properties.length; i++) {
var prop = Two.RoundedRectangle.Properties[i];
assert.equal(
path[prop],
properties[i],
'Can get property ' + prop + ' correctly.'
);
path[prop] = properties[i] * 2;
assert.equal(
path[prop],
properties[i] * 2,
'Can set property ' + prop + ' correctly.'
);
assert.equal(
path['_flag' + prop.charAt(0).toUpperCase() + prop.slice(1)],
true,
'Set ' + prop + "'s property flag correctly."
);
}
radius = new Two.Anchor(4, 6);
path.radius = radius.clone();
assert.equal(
radius.equals(path.radius),
true,
'Can get and set radius as a vector correctly.'
);
path._update();
path.flagReset();
for (var i = 0; i < Two.RoundedRectangle.Properties.length; i++) {
var prop = Two.RoundedRectangle.Properties[i];
assert.equal(
path['_flag' + prop.charAt(0).toUpperCase() + prop.slice(1)],
false,
'Reset flag ' + prop + ' correctly.'
);
}
assert.equal(path._flagRadius, false, 'Reset flag radius correctly.');
path.closed = false;
path._update();
assert.equal(
path.vertices[0].equals(path.vertices[path.vertices.length - 1]),
true,
'RoundedRectangle has standardized vertex generation when shape is not closed'
);
});
QUnit.test('Two.RoundedRectangle Object Conversion', function (assert) {
assert.expect(10);
// Create original rounded rectangle
var roundedRect = new Two.RoundedRectangle(100, 100, 50, 75, 8);
roundedRect.fill = '#0000ff';
roundedRect.stroke = '#ff00ff';
roundedRect.linewidth = 3;
roundedRect.id = 'my-rounded-rect';
// Convert to object
var obj = roundedRect.toObject();
// Test object has expected properties
assert.equal(
typeof obj,
'object',
'Two.RoundedRectangle.toObject creates an object'
);
assert.equal(obj.width, 50, 'Two.RoundedRectangle.toObject preserves width');
assert.equal(
obj.height,
75,
'Two.RoundedRectangle.toObject preserves height'
);
assert.equal(obj.radius, 8, 'Two.RoundedRectangle.toObject preserves radius');
assert.equal(
obj.fill,
'#0000ff',
'Two.RoundedRectangle.toObject preserves fill'
);
assert.equal(
obj.id,
'my-rounded-rect',
'Two.RoundedRectangle.toObject preserves id'
);
// Create new rounded rectangle from object
var newRoundedRect = Two.RoundedRectangle.fromObject(obj);
// Test new rounded rectangle matches original
assert.equal(
newRoundedRect.width,
roundedRect.width,
'Two.RoundedRectangle.fromObject preserves width'
);
assert.equal(
newRoundedRect.height,
roundedRect.height,
'Two.RoundedRectangle.fromObject preserves height'
);
assert.equal(
newRoundedRect.radius,
roundedRect.radius,
'Two.RoundedRectangle.fromObject preserves radius'
);
// Test copy method
var copiedRoundedRect = new Two.RoundedRectangle().copy(roundedRect);
assert.deepEqual(
{ ...copiedRoundedRect.toObject(), id: roundedRect.id },
roundedRect.toObject(),
'Two.RoundedRectangle.copy creates identical rounded rectangle'
);
});
QUnit.test(
'Two.RoundedRectangle Vector Radius Construction',
function (assert) {
assert.expect(6);
var width = 100;
var height = 80;
var rx = 10;
var ry = 15;
var vectorRadius = new Two.Vector(rx, ry);
var rect = new Two.RoundedRectangle(0, 0, width, height, vectorRadius);
assert.equal(rect.width, width, 'Width set correctly with Vector radius');
assert.equal(
rect.height,
height,
'Height set correctly with Vector radius'
);
assert.equal(
rect.radius instanceof Two.Vector,
true,
'Radius is Vector instance'
);
assert.equal(rect.radius.x, rx, 'Vector radius x value set correctly');
assert.equal(rect.radius.y, ry, 'Vector radius y value set correctly');
rect._update();
var expectedX1 = width / 2 - rx;
var expectedY1 = -height / 2;
assert.equal(
rect.vertices[1].x === expectedX1 && rect.vertices[1].y === expectedY1,
true,
'Vertices calculated correctly with Vector radius'
);
}
);
QUnit.test('Two.Star', function (assert) {
assert.expect(Two.Star.Properties.length * 4 + 1);
var innerRadius = 50;
var outerRadius = 75;
var sides = 5;
var properties = [innerRadius, outerRadius, sides];
var path = new Two.Star(0, 0, innerRadius, outerRadius, sides);
for (var i = 0; i < Two.Star.Properties.length; i++) {
var prop = Two.Star.Properties[i];
assert.equal(
path[prop],
properties[i],
'Can get property ' + prop + ' correctly.'
);
path[prop] = properties[i] * 2;
assert.equal(
path[prop],
properties[i] * 2,
'Can set property ' + prop + ' correctly.'
);
assert.equal(
path['_flag' + prop.charAt(0).toUpperCase() + prop.slice(1)],
true,
'Set ' + prop + "'s property flag correctly."
);
}
path._update();
path.flagReset();
for (var i = 0; i < Two.Star.Properties.length; i++) {
var prop = Two.Star.Properties[i];
assert.equal(
path['_flag' + prop.charAt(0).toUpperCase() + prop.slice(1)],
false,
'Reset flag ' + prop + ' correctly.'
);
}
path.closed = false;
path._update();
assert.equal(
path.vertices[0].equals(path.vertices[path.vertices.length - 1]),
true,
'Star has standardized vertex generation when shape is not closed'
);
});
QUnit.test('Two.Star Object Conversion', function (assert) {
assert.expect(10);
// Create original star
var star = new Two.Star(100, 100, 50, 75, 5);
star.fill = '#0000ff';
star.stroke = '#ff00ff';
star.linewidth = 3;
star.id = 'my-star';
// Convert to object
var obj = star.toObject();
// Test object has expected properties
assert.equal(typeof obj, 'object', 'Two.Star.toObject creates an object');
assert.equal(obj.innerRadius, 50, 'Two.Star.toObject preserves innerRadius');
assert.equal(obj.outerRadius, 75, 'Two.Star.toObject preserves outerRadius');
assert.equal(obj.sides, 5, 'Two.Star.toObject preserves sides');
assert.equal(obj.fill, '#0000ff', 'Two.Star.toObject preserves fill');
assert.equal(obj.id, 'my-star', 'Two.Star.toObject preserves id');
// Create new star from object
var newStar = Two.Star.fromObject(obj);
// Test new star matches original
assert.equal(
newStar.innerRadius,
star.innerRadius,
'Two.Star.fromObject preserves innerRadius'
);
assert.equal(
newStar.outerRadius,
star.outerRadius,
'Two.Star.fromObject preserves outerRadius'
);
assert.equal(
newStar.sides,
star.sides,
'Two.Star.fromObject preserves sides'
);
// Test copy method
var copiedStar = new Two.Star().copy(star);
assert.deepEqual(
{ ...copiedStar.toObject(), id: star.id },
star.toObject(),
'Two.Star.copy creates identical star'
);
});
================================================
FILE: tests/suite/svg-interpreter.js
================================================
/**
* Tests Two.js Utilities related to Svg Interpretation:
* + two.load()
* + two.interpret()
* + polygon.subdivide()
*/
(function () {
QUnit.module('SvgInterpreter');
QUnit.test('Two.load', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
width: 400,
height: 400,
});
two.load('./images/interpretation/D.svg', function (scene, svg) {
const shape = scene.children[0];
shape.center();
var answer = {
children: [
{
vertices: [
{
x: -77.9515,
y: 150,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.8935,
y: -150,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 77.899, y: 0 } },
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: -81.117,
command: 'C',
relative: true,
controls: { left: { x: 0, y: -67.354 }, right: { x: 0, y: 0 } },
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: 73.965,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -75.976 },
right: { x: -0.001, y: 75.976 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.9515,
y: 150,
command: 'C',
relative: true,
controls: { left: { x: 77.957, y: 0 }, right: { x: 0, y: 0 } },
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
],
fill: 'none',
stroke: '#333333',
linewidth: 10,
opacity: 1,
visible: true,
cap: 'round',
join: 'round',
miter: '10',
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: -3.54003907432e-6, y: 0 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
},
],
translation: { x: 200, y: 200 },
rotation: 0,
scale: { x: 1, y: 1 },
opacity: 1,
className: '',
mask: {
vertices: [
{
x: 0,
y: 0,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 400,
y: 0,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 400,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 0,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
],
fill: '#fff',
stroke: '#000',
linewidth: 1,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: -200.0005035400391, y: -200 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
width: 400,
height: 400,
origin: { x: -200, y: -200 },
},
};
shape.translation.set(two.width / 2, two.height / 2);
two.add(shape).update();
assert.ok(
QUnit.Utils.shapeEquals(answer, shape),
'Two.load loads SVG files properly.'
);
assert.done();
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement, svg]);
});
});
QUnit.test('Two.interpret', function (assert) {
assert.expect(10);
assert.done = assert.async(10);
(function () {
var two = new Two({
width: 400,
height: 400,
});
QUnit.Utils.get('./images/interpretation/D.svg', function (resp) {
var answer = {
children: [
{
vertices: [
{
x: -77.9515,
y: 150,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: -77.8935,
y: -150,
command: 'L',
relative: true,
controls: {
left: { x: 0, y: 0 },
right: { x: 77.899, y: 0 },
},
},
{
x: 77.9515,
y: -81.117,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -67.354 },
right: { x: 0, y: 0 },
},
},
{
x: 77.9515,
y: 73.965,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -75.976 },
right: { x: -0.001, y: 75.976 },
},
},
{
x: -77.9515,
y: 150,
command: 'C',
relative: true,
controls: {
left: { x: 77.957, y: 0 },
right: { x: 0, y: 0 },
},
},
],
fill: 'none',
stroke: '#333333',
linewidth: 10,
opacity: 1,
visible: true,
cap: 'round',
join: 'round',
miter: '10',
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: 200.0005, y: 200 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
},
],
translation: { x: 0, y: 0 },
rotation: 0,
scale: { x: 1, y: 1 },
opacity: 1,
className: '',
mask: {
vertices: [
{
x: 0,
y: 0,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 0,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 0,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
],
fill: '#fff',
stroke: '#000',
linewidth: 1,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: 0, y: 0 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
width: 400,
height: 400,
origin: { x: -200, y: -200 },
},
};
var svg = QUnit.Utils.textToDOM(resp)[0];
var shape = two.interpret(svg);
two.update();
assert.ok(
QUnit.Utils.shapeEquals(answer, shape),
'Two.interpret imports properly.'
);
assert.done();
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement, svg]);
});
})();
(function () {
var two = new Two({
width: 400,
height: 400,
});
QUnit.Utils.get('./images/interpretation/K.svg', function (resp) {
var answer = {
children: [
{
vertices: [
{
x: 416.146,
y: 350,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 415.862,
y: 650,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
],
fill: 'none',
stroke: '#333333',
linewidth: 10,
opacity: 1,
visible: true,
cap: 'round',
join: 'round',
miter: '10',
closed: false,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: -300, y: -300 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
},
{
vertices: [
{
x: 570.858,
y: 350,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 415.961,
y: 544.669,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
],
fill: 'none',
stroke: '#333333',
linewidth: 10,
opacity: 1,
visible: true,
cap: 'round',
join: 'round',
miter: '10',
closed: false,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: -300, y: -300 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
},
{
vertices: [
{
x: 469.29,
y: 477.828,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 584.138,
y: 650,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
],
fill: 'none',
stroke: '#333333',
linewidth: 10,
opacity: 1,
visible: true,
cap: 'round',
join: 'round',
miter: '10',
closed: false,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: -300, y: -300 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
},
],
translation: { x: 0, y: 0 },
rotation: 0,
scale: { x: 1, y: 1 },
opacity: 1,
className: '',
mask: {
vertices: [
{
x: 0,
y: 0,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 0,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 0,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
],
fill: '#fff',
stroke: '#000',
linewidth: 1,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: 0, y: 0 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
width: 400,
height: 400,
origin: { x: -200, y: -200 },
},
};
var svg = QUnit.Utils.textToDOM(resp)[0];
var shape = two.interpret(svg);
two.update();
assert.ok(
QUnit.Utils.shapeEquals(answer, shape),
'Two.interpret imports properly.'
);
assert.done();
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement, svg]);
});
})();
(function () {
var two = new Two({
width: 400,
height: 400,
});
QUnit.Utils.get('./images/interpretation/circle.svg', function (resp) {
var answer = {
children: [
{
vertices: [
{
x: 100,
y: 0,
command: 'M',
relative: true,
controls: {
left: { x: 3.381768755491e-15, y: -55.2284749831 },
right: { x: 3.381768755491e-15, y: 55.2284749831 },
},
},
{
x: 6.123233995737e-15,
y: 100,
command: 'C',
relative: true,
controls: {
left: { x: 55.2284749831, y: 0 },
right: { x: -55.2284749831, y: 6.763537510982e-15 },
},
},
{
x: -100,
y: 1.224646799147e-14,
command: 'C',
relative: true,
controls: {
left: { x: 3.381768755491e-15, y: 55.2284749831 },
right: { x: -1.014530626647e-14, y: -55.2284749831 },
},
},
{
x: -1.836970198721e-14,
y: -100,
command: 'C',
relative: true,
controls: {
left: { x: -55.2284749831, y: 6.763537510982e-15 },
right: { x: 55.2284749831, y: -1.352707502196e-14 },
},
},
],
fill: '#EF4142',
stroke: '#00AEEF',
linewidth: 25,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: '10',
closed: true,
curved: true,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: 200, y: 200 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
radius: 100,
},
],
translation: { x: 0, y: 0 },
rotation: 0,
scale: { x: 1, y: 1 },
opacity: 1,
className: '',
mask: {
vertices: [
{
x: 0,
y: 0,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 0,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 0,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
],
fill: '#fff',
stroke: '#000',
linewidth: 1,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: 0, y: 0 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
width: 400,
height: 400,
origin: { x: -200, y: -200 },
},
};
var svg = QUnit.Utils.textToDOM(resp)[0];
var shape = two.interpret(svg);
two.update();
assert.ok(
QUnit.Utils.shapeEquals(answer, shape),
'Two.interpret imports properly.'
);
assert.done();
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement, svg]);
});
})();
(function () {
var two = new Two({
width: 400,
height: 400,
});
QUnit.Utils.get('./images/interpretation/rect.svg', function (resp) {
var answer = {
children: [
{
vertices: [
{
x: -100,
y: -100,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 100,
y: -100,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 100,
y: 100,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: -100,
y: 100,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
],
fill: '#F7941E',
linewidth: 1,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: 200, y: 200 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
width: 200,
height: 200,
origin: { x: 0, y: 0 },
},
],
translation: { x: 0, y: 0 },
rotation: 0,
scale: { x: 1, y: 1 },
opacity: 1,
className: '',
mask: {
vertices: [
{
x: 0,
y: 0,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 0,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 0,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
],
fill: '#fff',
stroke: '#000',
linewidth: 1,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: 0, y: 0 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
width: 400,
height: 400,
origin: { x: -200, y: -200 },
},
};
var svg = QUnit.Utils.textToDOM(resp)[0];
var shape = two.interpret(svg);
two.update();
assert.ok(
QUnit.Utils.shapeEquals(answer, shape),
'Two.interpret imports properly.'
);
assert.done();
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement, svg]);
});
})();
(function () {
var two = new Two({
width: 400,
height: 400,
});
QUnit.Utils.get('./images/interpretation/ellipse.svg', function (resp) {
var answer = {
children: [
{
vertices: [
{
x: 150,
y: 0,
command: 'M',
relative: true,
controls: {
left: { x: 5.072653133236e-15, y: -42.469040408 },
right: { x: 5.072653133236e-15, y: 42.469040408 },
},
},
{
x: 9.184850993605e-15,
y: 76.897,
command: 'C',
relative: true,
controls: {
left: { x: 82.842712475, y: 0 },
right: { x: -82.842712475, y: 5.20095743982e-15 },
},
},
{
x: -150,
y: 9.417166491403e-15,
command: 'C',
relative: true,
controls: {
left: { x: 5.072653133236e-15, y: 42.469040408 },
right: { x: -1.521795939971e-14, y: -42.469040408 },
},
},
{
x: -2.755455298082e-14,
y: -76.897,
command: 'C',
relative: true,
controls: {
left: { x: -82.842712475, y: 5.20095743982e-15 },
right: { x: 82.842712475, y: -1.040191487964e-14 },
},
},
],
fill: '#92278F',
linewidth: 1,
opacity: 0.5,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: true,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: 200, y: 200 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
width: 300,
height: 153.794,
},
],
translation: { x: 0, y: 0 },
rotation: 0,
scale: { x: 1, y: 1 },
opacity: 1,
className: '',
mask: {
vertices: [
{
x: 0,
y: 0,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 0,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 0,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
],
fill: '#fff',
stroke: '#000',
linewidth: 1,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: 0, y: 0 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
width: 400,
height: 400,
origin: { x: -200, y: -200 },
},
};
var svg = QUnit.Utils.textToDOM(resp)[0];
var shape = two.interpret(svg);
two.update();
assert.ok(
QUnit.Utils.shapeEquals(answer, shape),
'Two.interpret imports properly.'
);
assert.done();
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement, svg]);
});
})();
(function () {
var two = new Two({
width: 400,
height: 400,
});
QUnit.Utils.get('./images/interpretation/polyline.svg', function (resp) {
var answer = {
children: [
{
vertices: [
{ x: 20.079, y: 104.42, command: 'M', relative: true },
{ x: 47.352, y: 104.42, command: 'L', relative: true },
{ x: 47.352, y: 90.785, command: 'L', relative: true },
{ x: 74.625, y: 90.785, command: 'L', relative: true },
{ x: 74.625, y: 104.42, command: 'L', relative: true },
{ x: 101.897, y: 104.42, command: 'L', relative: true },
{ x: 101.897, y: 70.33, command: 'L', relative: true },
{ x: 129.17, y: 70.33, command: 'L', relative: true },
{ x: 129.17, y: 104.42, command: 'L', relative: true },
{ x: 156.442, y: 104.42, command: 'L', relative: true },
{ x: 156.442, y: 49.875, command: 'L', relative: true },
{ x: 183.715, y: 49.875, command: 'L', relative: true },
{ x: 183.715, y: 104.42, command: 'L', relative: true },
{ x: 210.988, y: 104.42, command: 'L', relative: true },
{ x: 210.988, y: 29.42, command: 'L', relative: true },
{ x: 238.26, y: 29.42, command: 'L', relative: true },
{ x: 238.26, y: 104.42, command: 'L', relative: true },
{ x: 265.534, y: 104.42, command: 'L', relative: true },
{ x: 265.534, y: 8.965, command: 'L', relative: true },
{ x: 292.805, y: 8.965, command: 'L', relative: true },
{ x: 292.805, y: 104.42, command: 'L', relative: true },
{ x: 320.079, y: 104.42, command: 'L', relative: true },
],
fill: 'none',
stroke: '#0000FF',
linewidth: 10,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: false,
curved: false,
automatic: true,
beginning: 0,
ending: 1,
className: '',
translation: { x: 29.921, y: 143.307 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
},
],
translation: { x: 0, y: 0 },
rotation: 0,
scale: { x: 1, y: 1 },
opacity: 1,
className: '',
mask: {
vertices: [
{
x: 0,
y: 0,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 0,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 0,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
],
fill: '#fff',
stroke: '#000',
linewidth: 1,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: 0, y: 0 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
width: 400,
height: 400,
origin: { x: -200, y: -200 },
},
};
var svg = QUnit.Utils.textToDOM(resp)[0];
var shape = two.interpret(svg);
two.update();
assert.ok(
QUnit.Utils.shapeEquals(answer, shape),
'Two.interpret imports properly.'
);
assert.done();
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement, svg]);
});
})();
(function () {
var two = new Two({
width: 400,
height: 400,
});
QUnit.Utils.get('./images/interpretation/polygon.svg', function (resp) {
var answer = {
children: [
{
vertices: [
{ x: 99.212, y: 21.26, command: 'M', relative: true },
{ x: 107.433, y: 45.638, command: 'L', relative: true },
{ x: 132.945, y: 45.638, command: 'L', relative: true },
{ x: 112.536, y: 60.945, command: 'L', relative: true },
{ x: 119.905, y: 85.323, command: 'L', relative: true },
{ x: 99.212, y: 70.867, command: 'L', relative: true },
{ x: 78.52, y: 85.323, command: 'L', relative: true },
{ x: 85.89, y: 60.945, command: 'L', relative: true },
{ x: 65.48, y: 45.638, command: 'L', relative: true },
{ x: 90.992, y: 45.638, command: 'L', relative: true },
],
fill: '#FF0000',
linewidth: 1,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: false,
automatic: true,
beginning: 0,
ending: 1,
className: '',
translation: { x: 29.921, y: 143.307 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
},
{
vertices: [
{ x: 240.945, y: 21.26, command: 'M', relative: true },
{ x: 271.559, y: 38.977, command: 'L', relative: true },
{ x: 271.559, y: 74.41, command: 'L', relative: true },
{ x: 240.945, y: 92.126, command: 'L', relative: true },
{ x: 210.331, y: 74.438, command: 'L', relative: true },
{ x: 210.331, y: 38.977, command: 'L', relative: true },
],
fill: '#00FF00',
linewidth: 1,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: false,
automatic: true,
beginning: 0,
ending: 1,
className: '',
translation: { x: 29.921, y: 143.307 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
},
],
translation: { x: 0, y: 0 },
rotation: 0,
scale: { x: 1, y: 1 },
opacity: 1,
className: '',
mask: {
vertices: [
{
x: 0,
y: 0,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 0,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 0,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
],
fill: '#fff',
stroke: '#000',
linewidth: 1,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: 0, y: 0 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
width: 400,
height: 400,
origin: { x: -200, y: -200 },
},
};
var svg = QUnit.Utils.textToDOM(resp)[0];
var shape = two.interpret(svg);
two.update();
assert.ok(
QUnit.Utils.shapeEquals(answer, shape),
'Two.interpret imports properly.'
);
assert.done();
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement, svg]);
});
})();
(function () {
var two = new Two({
width: 400,
height: 400,
});
QUnit.Utils.get(
'./images/interpretation/linear-gradient.svg',
function (resp) {
var answer = {
children: [
{
vertices: [
{
x: -100,
y: -100,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 100,
y: -100,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 100,
y: 100,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: -100,
y: 100,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
],
fill: {
stops: [
{ offset: 0, opacity: 1, color: '#000000' },
{ offset: 0.33, opacity: 1, color: '#FFF200' },
{ offset: 0.66, opacity: 1, color: '#EC008C' },
{ offset: 1, opacity: 1, color: '#00AEEF' },
],
spread: 'pad',
left: { x: -100, y: 0 },
right: { x: 100, y: 0 },
},
linewidth: 1,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: 200, y: 200 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
width: 200,
height: 200,
origin: { x: 0, y: 0 },
},
],
translation: { x: 0, y: 0 },
rotation: 0,
scale: 1,
opacity: 1,
className: '',
mask: {
vertices: [
{
x: 0,
y: 0,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 0,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 0,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
],
fill: '#fff',
stroke: '#000',
linewidth: 1,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: 0, y: 0 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
width: 400,
height: 400,
origin: { x: -200, y: -200 },
},
};
var svg = QUnit.Utils.textToDOM(resp)[0];
var shape = two.interpret(svg);
two.update();
assert.ok(
QUnit.Utils.shapeEquals(answer, shape),
'Two.interpret imports properly.'
);
assert.done();
QUnit.Utils.addElemToTest(assert.test, [
two.renderer.domElement,
svg,
]);
}
);
})();
(function () {
var two = new Two({
width: 400,
height: 400,
});
QUnit.Utils.get(
'./images/interpretation/radial-gradient.svg',
function (resp) {
var answer = {
children: [
{
stops: [
{ offset: 0, opacity: 1, color: '#000000' },
{ offset: 0.33, opacity: 1, color: '#FFF200' },
{ offset: 0.66, opacity: 1, color: '#EC008C' },
{ offset: 1, opacity: 1, color: '#00AEEF' },
],
spread: 'pad',
radius: 100,
center: { x: 0, y: 0 },
focal: { x: 0, y: 0 },
},
{
vertices: [
{
x: -100,
y: -100,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 100,
y: -100,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 100,
y: 100,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: -100,
y: 100,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
],
fill: {
stops: [
{ offset: 0, opacity: 1, color: '#000000' },
{ offset: 0.33, opacity: 1, color: '#FFF200' },
{ offset: 0.66, opacity: 1, color: '#EC008C' },
{ offset: 1, opacity: 1, color: '#00AEEF' },
],
spread: 'pad',
radius: 100,
center: { x: 0, y: 0 },
focal: { x: 0, y: 0 },
},
linewidth: 1,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: 200, y: 200 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
width: 200,
height: 200,
origin: { x: 0, y: 0 },
},
],
translation: { x: 0, y: 0 },
rotation: 0,
scale: 1,
opacity: 1,
className: '',
mask: {
vertices: [
{
x: 0,
y: 0,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 0,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 400,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
{
x: 0,
y: 400,
command: 'L',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
},
],
fill: '#fff',
stroke: '#000',
linewidth: 1,
opacity: 1,
visible: true,
cap: 'butt',
join: 'miter',
miter: 4,
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
className: '',
translation: { x: 0, y: 0 },
rotation: 0,
scale: 1,
skewX: 0,
skewY: 0,
width: 400,
height: 400,
origin: { x: -200, y: -200 },
},
};
var svg = QUnit.Utils.textToDOM(resp)[0];
var shape = two.interpret(svg);
two.update();
assert.ok(
QUnit.Utils.shapeEquals(answer, shape),
'Two.interpret imports properly.'
);
assert.done();
QUnit.Utils.addElemToTest(assert.test, [
two.renderer.domElement,
svg,
]);
}
);
})();
(function () {
var two = new Two({
width: 400,
height: 400,
type: Two.Types.canvas, //The font-size problem only occurs in canvas renderer
});
QUnit.Utils.get('./images/interpretation/text.svg', function (resp) {
var svg = QUnit.Utils.textToDOM(resp)[0];
const shape = two.interpret(svg);
two.update();
assert.equal(
shape.children[1].size,
144,
'Font size is extracted correctly'
);
assert.done();
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement, svg]);
});
})();
});
QUnit.test('Two.subdivide', function (assert) {
assert.expect(3);
assert.done = assert.async(3);
(function () {
var two = new Two({
width: 400,
height: 400,
});
QUnit.Utils.get('./images/interpretation/D.svg', function (resp) {
var answer = {
children: [
{
isShape: true,
translation: { x: 200, y: 200 },
scale: 1,
skewX: 0,
skewY: 0,
vertices: [
{
x: -77.9515,
y: 150,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.950922,
y: 147.007938,
command: 'C',
relative: true,
controls: {
left: { x: -0.000378, y: 1.953999 },
right: { x: 0.000377, y: -1.954 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.949281,
y: 138.520252,
command: 'C',
relative: true,
controls: {
left: { x: -0.000709, y: 3.663749 },
right: { x: 0.000708, y: -3.66375 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.946719,
y: 125.269692,
command: 'C',
relative: true,
controls: {
left: { x: -0.000992, y: 5.129248 },
right: { x: 0.000991, y: -5.129249 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.943378,
y: 107.989008,
command: 'C',
relative: true,
controls: {
left: { x: -0.001228, y: 6.350498 },
right: { x: 0.001227, y: -6.350499 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.9394,
y: 87.41095,
command: 'C',
relative: true,
controls: {
left: { x: -0.001417, y: 7.327498 },
right: { x: 0.001416, y: -7.327499 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.934926,
y: 64.268267,
command: 'C',
relative: true,
controls: {
left: { x: -0.001559, y: 8.060248 },
right: { x: 0.001558, y: -8.060249 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.930097,
y: 39.29371,
command: 'C',
relative: true,
controls: {
left: { x: -0.001653, y: 8.548748 },
right: { x: 0.001652, y: -8.548749 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.925056,
y: 13.220028,
command: 'C',
relative: true,
controls: {
left: { x: -0.0017, y: 8.792998 },
right: { x: 0.001699, y: -8.792999 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.919945,
y: -13.220029,
command: 'C',
relative: true,
controls: {
left: { x: -0.0017, y: 8.792998 },
right: { x: 0.001699, y: -8.792999 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.914904,
y: -39.293711,
command: 'C',
relative: true,
controls: {
left: { x: -0.001653, y: 8.548748 },
right: { x: 0.001652, y: -8.548749 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.910075,
y: -64.268268,
command: 'C',
relative: true,
controls: {
left: { x: -0.001559, y: 8.060248 },
right: { x: 0.001558, y: -8.060249 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.905601,
y: -87.410951,
command: 'C',
relative: true,
controls: {
left: { x: -0.001417, y: 7.327498 },
right: { x: 0.001416, y: -7.327499 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.901623,
y: -107.989009,
command: 'C',
relative: true,
controls: {
left: { x: -0.001228, y: 6.350498 },
right: { x: 0.001227, y: -6.350499 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.898282,
y: -125.269693,
command: 'C',
relative: true,
controls: {
left: { x: -0.000992, y: 5.129248 },
right: { x: 0.000991, y: -5.129249 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.89572,
y: -138.520253,
command: 'C',
relative: true,
controls: {
left: { x: -0.000709, y: 3.663749 },
right: { x: 0.000708, y: -3.66375 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.894079,
y: -147.007939,
command: 'C',
relative: true,
controls: {
left: { x: -0.000378, y: 1.953999 },
right: { x: 0.000377, y: -1.954 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.8935,
y: -150,
command: 'L',
relative: true,
controls: {
left: { x: 0, y: 0 },
right: { x: 4.582294, y: 0 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -64.162005,
y: -149.971042,
command: 'C',
relative: true,
controls: {
left: { x: -4.566745, y: -0.023669 },
right: { x: 4.566744, y: 0.023668 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -50.524783,
y: -149.831817,
command: 'C',
relative: true,
controls: {
left: { x: -4.519446, y: -0.073511 },
right: { x: 4.519445, y: 0.07351 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -37.077083,
y: -149.503806,
command: 'C',
relative: true,
controls: {
left: { x: -4.440397, y: -0.149527 },
right: { x: 4.440396, y: 0.149526 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -23.914153,
y: -148.908487,
command: 'C',
relative: true,
controls: {
left: { x: -4.329599, y: -0.251716 },
right: { x: 4.329598, y: 0.251715 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -11.131244,
y: -147.967337,
command: 'C',
relative: true,
controls: {
left: { x: -4.18705, y: -0.38008 },
right: { x: 4.187049, y: 0.380079 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 1.176396,
y: -146.601836,
command: 'C',
relative: true,
controls: {
left: { x: -4.012753, y: -0.534617 },
right: { x: 4.012752, y: 0.534616 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 12.913519,
y: -144.733463,
command: 'C',
relative: true,
controls: {
left: { x: -3.806705, y: -0.715328 },
right: { x: 3.806704, y: 0.715327 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 23.984874,
y: -142.283695,
command: 'C',
relative: true,
controls: {
left: { x: -3.568908, y: -0.922213 },
right: { x: 3.568907, y: 0.922212 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 34.295215,
y: -139.174012,
command: 'C',
relative: true,
controls: {
left: { x: -3.299361, y: -1.155272 },
right: { x: 3.29936, y: 1.155271 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 43.74929,
y: -135.325891,
command: 'C',
relative: true,
controls: {
left: { x: -2.998065, y: -1.414505 },
right: { x: 2.998064, y: 1.414504 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 52.251852,
y: -130.660812,
command: 'C',
relative: true,
controls: {
left: { x: -2.665019, y: -1.699911 },
right: { x: 2.665018, y: 1.69991 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 59.707652,
y: -125.100252,
command: 'C',
relative: true,
controls: {
left: { x: -2.300223, y: -2.011492 },
right: { x: 2.300222, y: 2.011491 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 66.02144,
y: -118.565691,
command: 'C',
relative: true,
controls: {
left: { x: -1.903678, y: -2.349246 },
right: { x: 1.903677, y: 2.349245 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 71.097967,
y: -110.978607,
command: 'C',
relative: true,
controls: {
left: { x: -1.475383, y: -2.713174 },
right: { x: 1.475382, y: 2.713173 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 74.841986,
y: -102.260478,
command: 'C',
relative: true,
controls: {
left: { x: -1.015339, y: -3.103275 },
right: { x: 1.015338, y: 3.103274 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.158246,
y: -92.332783,
command: 'C',
relative: true,
controls: {
left: { x: -0.523544, y: -3.519551 },
right: { x: 0.523543, y: 3.51955 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: -81.117,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -3.962001 },
right: { x: 0, y: 0 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: -80.312569,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -0.530708 },
right: { x: 0, y: 0.530707 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: -77.96623,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -1.027939 },
right: { x: 0, y: 1.027938 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: -74.178413,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -1.491694 },
right: { x: 0, y: 1.491693 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: -69.049548,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -1.921971 },
right: { x: 0, y: 1.92197 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: -62.680068,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -2.318771 },
right: { x: 0, y: 2.31877 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: -55.170402,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -2.682095 },
right: { x: 0, y: 2.682094 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: -46.620981,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -3.011941 },
right: { x: 0, y: 3.01194 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: -37.132235,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -3.308311 },
right: { x: 0, y: 3.30831 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: -26.804596,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -3.571203 },
right: { x: 0, y: 3.571202 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: -15.738494,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -3.800619 },
right: { x: 0, y: 3.800618 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: -4.034361,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -3.996558 },
right: { x: 0, y: 3.996557 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: 8.207375,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -4.15902 },
right: { x: 0, y: 4.159019 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: 20.886281,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -4.288005 },
right: { x: 0, y: 4.288004 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: 33.901927,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -4.383514 },
right: { x: 0, y: 4.383513 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: 47.153883,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -4.445545 },
right: { x: 0, y: 4.445544 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: 60.541717,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -4.474099 },
right: { x: 0, y: 4.474098 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.9515,
y: 73.964999,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -4.469177 },
right: { x: -0.000059, y: 4.469176 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 77.158078,
y: 86.599903,
command: 'C',
relative: true,
controls: {
left: { x: 0.523601, y: -3.95924 },
right: { x: -0.523602, y: 3.959239 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 74.841617,
y: 97.751317,
command: 'C',
relative: true,
controls: {
left: { x: 1.015418, y: -3.480184 },
right: { x: -1.015419, y: 3.480183 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 71.09729,
y: 107.511883,
command: 'C',
relative: true,
controls: {
left: { x: 1.475511, y: -3.032008 },
right: { x: -1.475512, y: 3.032007 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 66.020271,
y: 115.974242,
command: 'C',
relative: true,
controls: {
left: { x: 1.903879, y: -2.614712 },
right: { x: -1.90388, y: 2.614711 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 59.705736,
y: 123.231036,
command: 'C',
relative: true,
controls: {
left: { x: 2.300522, y: -2.228298 },
right: { x: -2.300523, y: 2.228297 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 52.248859,
y: 129.374905,
command: 'C',
relative: true,
controls: {
left: { x: 2.665441, y: -1.872763 },
right: { x: -2.665442, y: 1.872762 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 43.744813,
y: 134.498493,
command: 'C',
relative: true,
controls: {
left: { x: 2.998634, y: -1.54811 },
right: { x: -2.998635, y: 1.548109 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 34.288774,
y: 138.694441,
command: 'C',
relative: true,
controls: {
left: { x: 3.300103, y: -1.254336 },
right: { x: -3.300104, y: 1.254335 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 23.975917,
y: 142.055389,
command: 'C',
relative: true,
controls: {
left: { x: 3.569847, y: -0.991444 },
right: { x: -3.569848, y: 0.991443 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 12.901414,
y: 144.67398,
command: 'C',
relative: true,
controls: {
left: { x: 3.807866, y: -0.759431 },
right: { x: -3.807867, y: 0.75943 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 1.160441,
y: 146.642855,
command: 'C',
relative: true,
controls: {
left: { x: 4.014161, y: -0.5583 },
right: { x: -4.014162, y: 0.558299 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -11.151827,
y: 148.054656,
command: 'C',
relative: true,
controls: {
left: { x: 4.18873, y: -0.388049 },
right: { x: -4.188731, y: 0.388048 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -23.940217,
y: 149.002024,
command: 'C',
relative: true,
controls: {
left: { x: 4.331575, y: -0.248678 },
right: { x: -4.331576, y: 0.248677 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -37.109554,
y: 149.5776,
command: 'C',
relative: true,
controls: {
left: { x: 4.442695, y: -0.140188 },
right: { x: -4.442696, y: 0.140187 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -50.564663,
y: 149.874028,
command: 'C',
relative: true,
controls: {
left: { x: 4.52209, y: -0.062578 },
right: { x: -4.522091, y: 0.062577 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -64.21037,
y: 149.983947,
command: 'C',
relative: true,
controls: {
left: { x: 4.56976, y: -0.015849 },
right: { x: -4.569761, y: 0.015848 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -77.9515,
y: 150,
command: 'C',
relative: true,
controls: {
left: { x: 4.585705, y: 0 },
right: { x: 0, y: 0 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
],
fill: 'none',
stroke: '#333333',
linewidth: 10,
opacity: 1,
visible: true,
cap: 'round',
join: 'round',
miter: '10',
closed: true,
curved: false,
automatic: false,
beginning: 0,
ending: 1,
dashes: [],
strokeAttenuation: true,
},
],
};
var svg = QUnit.Utils.textToDOM(resp)[0];
var shape = two.interpret(svg).subdivide();
var group = two.makeGroup();
group.translation.copy(shape.children[0].translation);
_.each(shape.children[0].vertices, function (v) {
var circle = new Two.Circle(v.x, v.y, 3);
circle.noStroke().fill = 'red';
group.add(circle);
});
two.update();
assert.ok(
QUnit.Utils.shapeEquals(answer, shape),
'Two.subdivide subdivides curveTo and lineTo properly.'
);
assert.done();
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement, svg]);
});
})();
(function () {
var two = new Two({
width: 400,
height: 400,
});
QUnit.Utils.get(
'./images/interpretation/compound-path.svg',
function (resp) {
var answer = {
isShape: true,
translation: { x: 0, y: 0 },
scale: { x: 1, y: 1 },
skewX: 0,
skewY: 0,
children: [
{
isShape: true,
translation: { x: 200, y: 200 },
scale: 1,
skewX: 0,
skewY: 0,
vertices: [
{
x: -120,
y: 0,
command: 'M',
relative: true,
controls: {
left: { x: 0, y: 0 },
right: { x: 0, y: 3.248764 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -119.542234,
y: 9.630749,
command: 'C',
relative: true,
controls: {
left: { x: -0.30285, y: -3.169407 },
right: { x: 0.302849, y: 3.169406 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -118.19687,
y: 19.002473,
command: 'C',
relative: true,
controls: {
left: { x: -0.591732, y: -3.076081 },
right: { x: 0.591731, y: 3.07608 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -116.005814,
y: 28.073266,
command: 'C',
relative: true,
controls: {
left: { x: -0.866645, y: -2.968787 },
right: { x: 0.866644, y: 2.968786 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -113.010971,
y: 36.801223,
command: 'C',
relative: true,
controls: {
left: { x: -1.12759, y: -2.847524 },
right: { x: 1.127589, y: 2.847523 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -109.254244,
y: 45.144441,
command: 'C',
relative: true,
controls: {
left: { x: -1.374567, y: -2.712294 },
right: { x: 1.374566, y: 2.712293 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -104.77754,
y: 53.061014,
command: 'C',
relative: true,
controls: {
left: { x: -1.607576, y: -2.563095 },
right: { x: 1.607575, y: 2.563094 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -99.622763,
y: 60.509037,
command: 'C',
relative: true,
controls: {
left: { x: -1.826616, y: -2.399927 },
right: { x: 1.826615, y: 2.399926 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -93.831816,
y: 67.446606,
command: 'C',
relative: true,
controls: {
left: { x: -2.031688, y: -2.222792 },
right: { x: 2.031687, y: 2.222791 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -87.446607,
y: 73.831815,
command: 'C',
relative: true,
controls: {
left: { x: -2.222792, y: -2.031688 },
right: { x: 2.222791, y: 2.031687 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -80.509038,
y: 79.622762,
command: 'C',
relative: true,
controls: {
left: { x: -2.399927, y: -1.826616 },
right: { x: 2.399926, y: 1.826615 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -73.061015,
y: 84.777539,
command: 'C',
relative: true,
controls: {
left: { x: -2.563095, y: -1.607576 },
right: { x: 2.563094, y: 1.607575 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -65.144442,
y: 89.254243,
command: 'C',
relative: true,
controls: {
left: { x: -2.712294, y: -1.374567 },
right: { x: 2.712293, y: 1.374566 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -56.801224,
y: 93.01097,
command: 'C',
relative: true,
controls: {
left: { x: -2.847524, y: -1.12759 },
right: { x: 2.847523, y: 1.127589 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -48.073267,
y: 96.005813,
command: 'C',
relative: true,
controls: {
left: { x: -2.968787, y: -0.866645 },
right: { x: 2.968786, y: 0.866644 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -39.002474,
y: 98.196869,
command: 'C',
relative: true,
controls: {
left: { x: -3.076081, y: -0.591732 },
right: { x: 3.07608, y: 0.591731 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -29.63075,
y: 99.542233,
command: 'C',
relative: true,
controls: {
left: { x: -3.169407, y: -0.30285 },
right: { x: 3.169406, y: 0.302849 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: 100,
command: 'C',
relative: true,
controls: {
left: { x: -3.248765, y: 0 },
right: { x: 0, y: 0 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: 98.005292,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 1.302666 },
right: { x: 0, y: -1.302667 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: 92.346834,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 2.442499 },
right: { x: 0, y: -2.4425 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: 83.513128,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 3.419499 },
right: { x: 0, y: -3.4195 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: 71.992672,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 4.233665 },
right: { x: 0, y: -4.233666 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: 58.273967,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 4.884998 },
right: { x: 0, y: -4.884999 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: 42.845511,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 5.373498 },
right: { x: 0, y: -5.373499 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: 26.195807,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 5.699165 },
right: { x: 0, y: -5.699166 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: 8.813352,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 5.861998 },
right: { x: 0, y: -5.861999 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: -8.813353,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 5.861998 },
right: { x: 0, y: -5.861999 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: -26.195808,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 5.699165 },
right: { x: 0, y: -5.699166 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: -42.845512,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 5.373498 },
right: { x: 0, y: -5.373499 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: -58.273968,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 4.884998 },
right: { x: 0, y: -4.884999 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: -71.992673,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 4.233665 },
right: { x: 0, y: -4.233666 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: -83.513129,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 3.419499 },
right: { x: 0, y: -3.4195 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: -92.346835,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 2.442499 },
right: { x: 0, y: -2.4425 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: -98.005293,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 1.302666 },
right: { x: 0, y: -1.302667 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -20,
y: -100,
command: 'L',
relative: true,
controls: {
left: { x: 0, y: 0 },
right: { x: -3.248765, y: 0 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -29.63075,
y: -99.542234,
command: 'C',
relative: true,
controls: {
left: { x: 3.169406, y: -0.30285 },
right: { x: -3.169407, y: 0.302849 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -39.002474,
y: -98.19687,
command: 'C',
relative: true,
controls: {
left: { x: 3.07608, y: -0.591732 },
right: { x: -3.076081, y: 0.591731 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -48.073267,
y: -96.005814,
command: 'C',
relative: true,
controls: {
left: { x: 2.968786, y: -0.866645 },
right: { x: -2.968787, y: 0.866644 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -56.801224,
y: -93.010971,
command: 'C',
relative: true,
controls: {
left: { x: 2.847523, y: -1.12759 },
right: { x: -2.847524, y: 1.127589 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -65.144442,
y: -89.254244,
command: 'C',
relative: true,
controls: {
left: { x: 2.712293, y: -1.374567 },
right: { x: -2.712294, y: 1.374566 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -73.061015,
y: -84.77754,
command: 'C',
relative: true,
controls: {
left: { x: 2.563094, y: -1.607576 },
right: { x: -2.563095, y: 1.607575 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -80.509038,
y: -79.622763,
command: 'C',
relative: true,
controls: {
left: { x: 2.399926, y: -1.826616 },
right: { x: -2.399927, y: 1.826615 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -87.446607,
y: -73.831816,
command: 'C',
relative: true,
controls: {
left: { x: 2.222791, y: -2.031688 },
right: { x: -2.222792, y: 2.031687 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -93.831816,
y: -67.446607,
command: 'C',
relative: true,
controls: {
left: { x: 2.031687, y: -2.222792 },
right: { x: -2.031688, y: 2.222791 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -99.622763,
y: -60.509038,
command: 'C',
relative: true,
controls: {
left: { x: 1.826615, y: -2.399927 },
right: { x: -1.826616, y: 2.399926 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -104.77754,
y: -53.061015,
command: 'C',
relative: true,
controls: {
left: { x: 1.607575, y: -2.563095 },
right: { x: -1.607576, y: 2.563094 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -109.254244,
y: -45.144442,
command: 'C',
relative: true,
controls: {
left: { x: 1.374566, y: -2.712294 },
right: { x: -1.374567, y: 2.712293 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -113.010971,
y: -36.801224,
command: 'C',
relative: true,
controls: {
left: { x: 1.127589, y: -2.847524 },
right: { x: -1.12759, y: 2.847523 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -116.005814,
y: -28.073267,
command: 'C',
relative: true,
controls: {
left: { x: 0.866644, y: -2.968787 },
right: { x: -0.866645, y: 2.968786 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -118.19687,
y: -19.002474,
command: 'C',
relative: true,
controls: {
left: { x: 0.591731, y: -3.076081 },
right: { x: -0.591732, y: 3.07608 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -119.542234,
y: -9.63075,
command: 'C',
relative: true,
controls: {
left: { x: 0.302849, y: -3.169407 },
right: { x: -0.30285, y: 3.169406 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -120,
y: 0,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -3.248765 },
right: { x: 0, y: 0 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -120,
y: 0,
command: 'Z',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: -100,
command: 'M',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: -98.005293,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -1.302667 },
right: { x: 0, y: 1.302666 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: -92.346835,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -2.4425 },
right: { x: 0, y: 2.442499 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: -83.513129,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -3.4195 },
right: { x: 0, y: 3.419499 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: -71.992673,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -4.233666 },
right: { x: 0, y: 4.233665 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: -58.273968,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -4.884999 },
right: { x: 0, y: 4.884998 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: -42.845512,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -5.373499 },
right: { x: 0, y: 5.373498 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: -26.195808,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -5.699166 },
right: { x: 0, y: 5.699165 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: -8.813353,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -5.861999 },
right: { x: 0, y: 5.861998 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: 8.813352,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -5.861999 },
right: { x: 0, y: 5.861998 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: 26.195807,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -5.699166 },
right: { x: 0, y: 5.699165 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: 42.845511,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -5.373499 },
right: { x: 0, y: 5.373498 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: 58.273967,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -4.884999 },
right: { x: 0, y: 4.884998 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: 71.992672,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -4.233666 },
right: { x: 0, y: 4.233665 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: 83.513128,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -3.4195 },
right: { x: 0, y: 3.419499 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: 92.346834,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -2.4425 },
right: { x: 0, y: 2.442499 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: 98.005292,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -1.302667 },
right: { x: 0, y: 1.302666 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: 100,
command: 'L',
relative: true,
controls: {
left: { x: 0, y: 0 },
right: { x: 3.248764, y: 0 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 29.630749,
y: 99.542233,
command: 'C',
relative: true,
controls: {
left: { x: -3.169407, y: 0.302849 },
right: { x: 3.169406, y: -0.30285 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 39.002473,
y: 98.196869,
command: 'C',
relative: true,
controls: {
left: { x: -3.076081, y: 0.591731 },
right: { x: 3.07608, y: -0.591732 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 48.073266,
y: 96.005813,
command: 'C',
relative: true,
controls: {
left: { x: -2.968787, y: 0.866644 },
right: { x: 2.968786, y: -0.866645 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 56.801223,
y: 93.01097,
command: 'C',
relative: true,
controls: {
left: { x: -2.847524, y: 1.127589 },
right: { x: 2.847523, y: -1.12759 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 65.144441,
y: 89.254243,
command: 'C',
relative: true,
controls: {
left: { x: -2.712294, y: 1.374566 },
right: { x: 2.712293, y: -1.374567 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 73.061014,
y: 84.777539,
command: 'C',
relative: true,
controls: {
left: { x: -2.563095, y: 1.607575 },
right: { x: 2.563094, y: -1.607576 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 80.509037,
y: 79.622762,
command: 'C',
relative: true,
controls: {
left: { x: -2.399927, y: 1.826615 },
right: { x: 2.399926, y: -1.826616 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 87.446606,
y: 73.831815,
command: 'C',
relative: true,
controls: {
left: { x: -2.222792, y: 2.031687 },
right: { x: 2.222791, y: -2.031688 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 93.831815,
y: 67.446606,
command: 'C',
relative: true,
controls: {
left: { x: -2.031688, y: 2.222791 },
right: { x: 2.031687, y: -2.222792 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 99.622762,
y: 60.509037,
command: 'C',
relative: true,
controls: {
left: { x: -1.826616, y: 2.399926 },
right: { x: 1.826615, y: -2.399927 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 104.777539,
y: 53.061014,
command: 'C',
relative: true,
controls: {
left: { x: -1.607576, y: 2.563094 },
right: { x: 1.607575, y: -2.563095 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 109.254243,
y: 45.144441,
command: 'C',
relative: true,
controls: {
left: { x: -1.374567, y: 2.712293 },
right: { x: 1.374566, y: -2.712294 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 113.01097,
y: 36.801223,
command: 'C',
relative: true,
controls: {
left: { x: -1.12759, y: 2.847523 },
right: { x: 1.127589, y: -2.847524 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 116.005813,
y: 28.073266,
command: 'C',
relative: true,
controls: {
left: { x: -0.866645, y: 2.968786 },
right: { x: 0.866644, y: -2.968787 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 118.196869,
y: 19.002473,
command: 'C',
relative: true,
controls: {
left: { x: -0.591732, y: 3.07608 },
right: { x: 0.591731, y: -3.076081 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 119.542233,
y: 9.630749,
command: 'C',
relative: true,
controls: {
left: { x: -0.30285, y: 3.169406 },
right: { x: 0.302849, y: -3.169407 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 120,
y: 0,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 3.248764 },
right: { x: 0, y: -3.248765 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 119.542233,
y: -9.63075,
command: 'C',
relative: true,
controls: {
left: { x: 0.302849, y: 3.169406 },
right: { x: -0.30285, y: -3.169407 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 118.196869,
y: -19.002474,
command: 'C',
relative: true,
controls: {
left: { x: 0.591731, y: 3.07608 },
right: { x: -0.591732, y: -3.076081 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 116.005813,
y: -28.073267,
command: 'C',
relative: true,
controls: {
left: { x: 0.866644, y: 2.968786 },
right: { x: -0.866645, y: -2.968787 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 113.01097,
y: -36.801224,
command: 'C',
relative: true,
controls: {
left: { x: 1.127589, y: 2.847523 },
right: { x: -1.12759, y: -2.847524 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 109.254243,
y: -45.144442,
command: 'C',
relative: true,
controls: {
left: { x: 1.374566, y: 2.712293 },
right: { x: -1.374567, y: -2.712294 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 104.777539,
y: -53.061015,
command: 'C',
relative: true,
controls: {
left: { x: 1.607575, y: 2.563094 },
right: { x: -1.607576, y: -2.563095 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 99.622762,
y: -60.509038,
command: 'C',
relative: true,
controls: {
left: { x: 1.826615, y: 2.399926 },
right: { x: -1.826616, y: -2.399927 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 93.831815,
y: -67.446607,
command: 'C',
relative: true,
controls: {
left: { x: 2.031687, y: 2.222791 },
right: { x: -2.031688, y: -2.222792 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 87.446606,
y: -73.831816,
command: 'C',
relative: true,
controls: {
left: { x: 2.222791, y: 2.031687 },
right: { x: -2.222792, y: -2.031688 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 80.509037,
y: -79.622763,
command: 'C',
relative: true,
controls: {
left: { x: 2.399926, y: 1.826615 },
right: { x: -2.399927, y: -1.826616 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 73.061014,
y: -84.77754,
command: 'C',
relative: true,
controls: {
left: { x: 2.563094, y: 1.607575 },
right: { x: -2.563095, y: -1.607576 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 65.144441,
y: -89.254244,
command: 'C',
relative: true,
controls: {
left: { x: 2.712293, y: 1.374566 },
right: { x: -2.712294, y: -1.374567 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 56.801223,
y: -93.010971,
command: 'C',
relative: true,
controls: {
left: { x: 2.847523, y: 1.127589 },
right: { x: -2.847524, y: -1.12759 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 48.073266,
y: -96.005814,
command: 'C',
relative: true,
controls: {
left: { x: 2.968786, y: 0.866644 },
right: { x: -2.968787, y: -0.866645 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 39.002473,
y: -98.19687,
command: 'C',
relative: true,
controls: {
left: { x: 3.07608, y: 0.591731 },
right: { x: -3.076081, y: -0.591732 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 29.630749,
y: -99.542234,
command: 'C',
relative: true,
controls: {
left: { x: 3.169406, y: 0.302849 },
right: { x: -3.169407, y: -0.30285 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 20,
y: -100,
command: 'C',
relative: true,
controls: {
left: { x: 3.248764, y: 0 },
right: { x: 0, y: 0 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
],
},
],
};
var svg = QUnit.Utils.textToDOM(resp)[0];
var shape = two.interpret(svg).subdivide();
var group = two.makeGroup();
group.translation.copy(shape.children[0].translation);
_.each(shape.children[0].vertices, function (v) {
var circle = new Two.Circle(v.x, v.y, 3);
circle.noStroke().fill = 'red';
group.add(circle);
});
two.update();
assert.ok(
QUnit.Utils.shapeEquals(answer, shape),
'Two.subdivide subdivides moveTo properly.'
);
assert.done();
QUnit.Utils.addElemToTest(assert.test, [
two.renderer.domElement,
svg,
]);
}
);
})();
(function () {
var two = new Two({
width: 400,
height: 400,
});
QUnit.Utils.get('./images/interpretation/donut.svg', function (resp) {
var answer = {
isShape: true,
translation: { x: 0, y: 0 },
scale: 1,
skewX: 0,
skewY: 0,
children: [
{
isShape: true,
translation: { x: 200, y: 200.16 },
scale: 1,
skewX: 0,
skewY: 0,
vertices: [
{
x: 0,
y: -121.338,
command: 'M',
relative: true,
controls: {
left: { x: 0, y: 0 },
right: { x: -16.753251, y: 0 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -47.230172,
y: -111.802641,
command: 'C',
relative: true,
controls: {
left: { x: 14.516671, y: -6.140047 },
right: { x: -14.516672, y: 6.140046 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -85.798875,
y: -85.798875,
command: 'C',
relative: true,
controls: {
left: { x: 10.978937, y: -10.978938 },
right: { x: -10.978938, y: 10.978937 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -111.802641,
y: -47.230172,
command: 'C',
relative: true,
controls: {
left: { x: 6.140046, y: -14.516672 },
right: { x: -6.140047, y: 14.516671 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -121.338,
y: 0,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -16.753251 },
right: { x: 0, y: 16.75325 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -111.802641,
y: 47.230171,
command: 'C',
relative: true,
controls: {
left: { x: -6.140047, y: -14.516672 },
right: { x: 6.140046, y: 14.516671 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -85.798875,
y: 85.798875,
command: 'C',
relative: true,
controls: {
left: { x: -10.978938, y: -10.978938 },
right: { x: 10.978937, y: 10.978937 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -47.230172,
y: 111.80264,
command: 'C',
relative: true,
controls: {
left: { x: -14.516672, y: -6.140047 },
right: { x: 14.516671, y: 6.140046 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 0,
y: 121.338,
command: 'C',
relative: true,
controls: {
left: { x: -16.753251, y: 0 },
right: { x: 16.75325, y: 0 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 47.230171,
y: 111.80264,
command: 'C',
relative: true,
controls: {
left: { x: -14.516672, y: 6.140046 },
right: { x: 14.516671, y: -6.140047 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 85.798875,
y: 85.798875,
command: 'C',
relative: true,
controls: {
left: { x: -10.978938, y: 10.978937 },
right: { x: 10.978937, y: -10.978938 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 111.80264,
y: 47.230171,
command: 'C',
relative: true,
controls: {
left: { x: -6.140047, y: 14.516671 },
right: { x: 6.140046, y: -14.516672 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 121.337999,
y: 0,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 16.75325 },
right: { x: 0, y: -16.753251 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 111.80264,
y: -47.230172,
command: 'C',
relative: true,
controls: {
left: { x: 6.140046, y: 14.516671 },
right: { x: -6.140047, y: -14.516672 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 85.798874,
y: -85.798875,
command: 'C',
relative: true,
controls: {
left: { x: 10.978937, y: 10.978937 },
right: { x: -10.978938, y: -10.978938 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 47.230171,
y: -111.802641,
command: 'C',
relative: true,
controls: {
left: { x: 14.516671, y: 6.140046 },
right: { x: -14.516672, y: -6.140047 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 0,
y: -121.338,
command: 'C',
relative: true,
controls: {
left: { x: 16.753249, y: 0 },
right: { x: 0, y: 0 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 0,
y: -121.338,
command: 'Z',
relative: true,
controls: { left: { x: 0, y: 0 }, right: { x: 0, y: 0 } },
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 0,
y: 62.155,
command: 'M',
relative: true,
controls: {
left: { x: 0, y: 0 },
right: { x: -8.58175, y: 0 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -24.193422,
y: 57.270515,
command: 'C',
relative: true,
controls: {
left: { x: 7.436109, y: 3.145234 },
right: { x: -7.43611, y: -3.145235 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -43.950125,
y: 43.950125,
command: 'C',
relative: true,
controls: {
left: { x: 5.623937, y: 5.623937 },
right: { x: -5.623938, y: -5.623938 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -57.270516,
y: 24.193421,
command: 'C',
relative: true,
controls: {
left: { x: 3.145234, y: 7.436109 },
right: { x: -3.145235, y: -7.43611 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -62.155,
y: 0,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: 8.58175 },
right: { x: 0, y: -8.58175 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -57.270516,
y: -24.193422,
command: 'C',
relative: true,
controls: {
left: { x: -3.145235, y: 7.436109 },
right: { x: 3.145234, y: -7.43611 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -43.950125,
y: -43.950125,
command: 'C',
relative: true,
controls: {
left: { x: -5.623938, y: 5.623937 },
right: { x: 5.623937, y: -5.623938 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: -24.193422,
y: -57.270516,
command: 'C',
relative: true,
controls: {
left: { x: -7.43611, y: 3.145234 },
right: { x: 7.436109, y: -3.145235 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 0,
y: -62.155,
command: 'C',
relative: true,
controls: {
left: { x: -8.58175, y: 0 },
right: { x: 8.58175, y: 0 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 24.193421,
y: -57.270516,
command: 'C',
relative: true,
controls: {
left: { x: -7.43611, y: -3.145235 },
right: { x: 7.436109, y: 3.145234 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 43.950124,
y: -43.950125,
command: 'C',
relative: true,
controls: {
left: { x: -5.623938, y: -5.623938 },
right: { x: 5.623937, y: 5.623937 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 57.270515,
y: -24.193422,
command: 'C',
relative: true,
controls: {
left: { x: -3.145235, y: -7.43611 },
right: { x: 3.145234, y: 7.436109 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 62.154999,
y: 0,
command: 'C',
relative: true,
controls: {
left: { x: 0, y: -8.58175 },
right: { x: 0, y: 8.58175 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 57.270515,
y: 24.193421,
command: 'C',
relative: true,
controls: {
left: { x: 3.145234, y: -7.43611 },
right: { x: -3.145235, y: 7.436109 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 43.950124,
y: 43.950125,
command: 'C',
relative: true,
controls: {
left: { x: 5.623937, y: -5.623938 },
right: { x: -5.623938, y: 5.623937 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 24.193421,
y: 57.270515,
command: 'C',
relative: true,
controls: {
left: { x: 7.436109, y: -3.145235 },
right: { x: -7.43611, y: 3.145234 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
{
x: 0,
y: 62.155,
command: 'C',
relative: true,
controls: {
left: { x: 8.58175, y: 0 },
right: { x: 0, y: 0 },
},
rx: 0,
ry: 0,
xAxisRotation: 0,
largeArcFlag: 0,
sweepFlag: 1,
},
],
},
],
};
var svg = QUnit.Utils.textToDOM(resp)[0];
var shape = two.interpret(svg).subdivide(3);
var group = two.makeGroup();
group.translation.copy(shape.children[0].translation);
_.each(shape.children[0].vertices, function (v) {
var circle = new Two.Circle(v.x, v.y, 3);
circle.noStroke().fill = 'red';
group.add(circle);
});
two.update();
assert.ok(
QUnit.Utils.shapeEquals(answer, shape),
'Two.subdivide subdivides holes properly.'
);
assert.done();
QUnit.Utils.addElemToTest(assert.test, [two.renderer.domElement, svg]);
});
})();
});
})();
================================================
FILE: tests/suite/svg.js
================================================
/**
* Tests Two.js Svg Rendering Functionality:
*/
(function () {
QUnit.module('SVGRenderer');
QUnit.test('Two.makeLine', function (assert) {
assert.expect(1);
var two = new Two({
width: 400,
height: 400,
});
var line = two.makeLine(0, 0, two.width, two.height);
two.update();
var elem = two.renderer.domElement.querySelector('#' + line.id);
assert.equal(
elem.getAttribute('d'),
'M 0 0 L 400 400 ',
'Two.makeLine applies d attribute properly.'
);
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.makeRectangle', function (assert) {
assert.expect(1);
var two = new Two({
width: 400,
height: 400,
});
var rect = two.makeRectangle(two.width / 2, two.height / 2, 100, 100);
two.update();
var elem = two.renderer.domElement.querySelector('#' + rect.id);
assert.equal(
elem.getAttribute('d'),
'M -50 -50 L 50 -50 L 50 50 L -50 50 Z ',
'Two.makeRectangle applies d attribute properly.'
);
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.makeEllipse', function (assert) {
assert.expect(1);
var two = new Two({
width: 400,
height: 400,
});
var ellipse = two.makeEllipse(two.width / 2, two.height / 2, 100, 100);
two.update();
var elem = two.renderer.domElement.querySelector('#' + ellipse.id);
assert.equal(
elem.getAttribute('d'),
'M 100 0 C 100 55.228474 55.228474 100 0 100 C -55.228475 100 -100 55.228474 -100 0 C -100.000001 -55.228475 -55.228475 -100 -0.000001 -100 C 55.228474 -100.000001 100 -55.228475 100 0 Z ',
'Two.makeEllipse applies d attribute properly.'
);
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.makeCircle', function (assert) {
assert.expect(1);
var two = new Two({
width: 400,
height: 400,
});
var circle = two.makeCircle(two.width / 2, two.height / 2, 50);
two.update();
var elem = two.renderer.domElement.querySelector('#' + circle.id);
assert.equal(
elem.getAttribute('d'),
'M 50 0 C 50 27.614237 27.614237 50 0 50 C -27.614238 50 -50 27.614237 -50 0 C -50.000001 -27.614238 -27.614238 -50 -0.000001 -50 C 27.614237 -50.000001 50 -27.614238 50 0 Z ',
'Two.makeCircle applies d attribute properly.'
);
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.makePoints', function (assert) {
assert.expect(1);
var two = new Two({
width: 400,
height: 400,
});
var points = two.makePoints(200, 200, 205, 200, 195, 200);
two.update();
var elem = two.renderer.domElement.querySelector('#' + points.id);
assert.equal(
elem.getAttribute('d'),
'M 200 199.5 a 0.5 0.5 0 1 0 0.001 0 ZM 205 199.5 a 0.5 0.5 0 1 0 0.001 0 ZM 195 199.5 a 0.5 0.5 0 1 0 0.001 0 Z',
'Two.makePoints applies d attribute properly.'
);
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.makePath', function (assert) {
assert.expect(2);
var two = new Two({
width: 400,
height: 400,
});
var amount = 20;
var points = [];
for (var i = 0; i < amount; i++) {
var pct = i / amount;
var x = pct * 300 + 50;
var y = i % 2 ? 25 : 75;
points.push(new Two.Anchor(x, y));
}
var poly = two.makePath(points, true);
var path = two.makePath();
two.update();
var elem = two.renderer.domElement.querySelector('#' + poly.id);
assert.equal(
elem.getAttribute('d'),
'M -142.5 25 L -127.5 -25 L -112.5 25 L -97.5 -25 L -82.5 25 L -67.5 -25 L -52.5 25 L -37.5 -25 L -22.5 25 L -7.5 -25 L 7.5 25 L 22.5 -25 L 37.5 25 L 52.5 -25 L 67.5 25 L 82.5 -25 L 97.5 25 L 112.5 -25 L 127.5 25 L 142.5 -25 ',
'Two.makePath applies d attribute properly.'
);
elem = two.renderer.domElement.querySelector('#' + path.id);
assert.equal(
elem.getAttribute('transform'),
'matrix(1 0 0 1 0 0)',
'Two.makePath applies transform attribute properly.'
);
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.makeCurve', function (assert) {
assert.expect(1);
var two = new Two({
width: 400,
height: 400,
});
var amount = 20;
var points = [];
for (var i = 0; i < amount; i++) {
var pct = i / amount;
var x = pct * 300 + 50;
var y = i % 2 ? 25 : 75;
points.push(new Two.Anchor(x, y));
}
var curve = two.makeCurve(points, true);
two.update();
var elem = two.renderer.domElement.querySelector('#' + curve.id);
assert.equal(
elem.getAttribute('d'),
'M -142.5 25 C -142.5 25 -144.726506 -25 -127.5 -25 C -110.273495 -25 -129.726506 24.999999 -112.5 25 C -95.273495 25 -114.726506 -25 -97.5 -25 C -80.273495 -25 -99.726506 24.999999 -82.5 25 C -65.273495 25 -84.726506 -25 -67.5 -25 C -50.273495 -25 -69.726506 24.999999 -52.5 25 C -35.273495 25 -54.726506 -25 -37.5 -25 C -20.273495 -25 -39.726506 24.999999 -22.5 25 C -5.273495 25 -24.726506 -25 -7.5 -25 C 9.726505 -25 -9.726506 24.999999 7.5 25 C 24.726505 25 5.273494 -25 22.5 -25 C 39.726505 -25 20.273494 24.999999 37.5 25 C 54.726505 25 35.273494 -25 52.5 -25 C 69.726505 -25 50.273494 24.999999 67.5 25 C 84.726505 25 65.273494 -25 82.5 -25 C 99.726505 -25 80.273494 24.999999 97.5 25 C 114.726505 25 95.273494 -25 112.5 -25 C 129.726505 -25 110.273494 24.999999 127.5 25 C 144.726505 25 142.5 -25 142.5 -25 ',
'The d attribute set properly.'
);
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.makeLinearGradient', function (assert) {
assert.expect(8);
var two = new Two({
width: 400,
height: 400,
});
var gradient = two.makeLinearGradient(
0,
-two.height / 2,
0,
two.height / 2,
new Two.Gradient.Stop(0, 'rgb(255, 100, 100)'),
new Two.Gradient.Stop(1, 'rgb(100, 100, 255)')
);
var rect = two.makeRectangle(
two.width / 2,
two.height / 2,
two.width / 4,
two.height / 4
);
rect.fill = gradient;
two.update();
var elem = two.renderer.domElement.querySelector('#' + gradient.id);
assert.equal(
elem.tagName,
'linearGradient',
'Two.LinearGradient renders as a tag.'
);
assert.equal(
parseFloat(elem.getAttribute('x1')),
0,
'The x1 attribute applied properly.'
);
assert.equal(
parseFloat(elem.getAttribute('y1')),
-200,
'The y1 attribute applied properly.'
);
assert.equal(
parseFloat(elem.getAttribute('x2')),
0,
'The x2 attribute applied properly.'
);
assert.equal(
parseFloat(elem.getAttribute('y2')),
200,
'The y2 attribute applied properly.'
);
assert.equal(
elem.getAttribute('spreadMethod'),
'pad',
'The spreadMethod attribute applied properly.'
);
assert.equal(
elem.getAttribute('gradientUnits'),
'objectBoundingBox',
'The gradientUnits attribute applied properly.'
);
assert.equal(
elem.innerHTML,
'',
'The innerHTML applied properly.'
);
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.makeRadialGradient', function (assert) {
assert.expect(9);
var two = new Two({
width: 400,
height: 400,
});
var gradient = two.makeRadialGradient(
0,
0,
two.width / 2,
new Two.Gradient.Stop(0, 'rgb(255, 100, 100)'),
new Two.Gradient.Stop(1, 'rgb(100, 100, 255)')
);
var rect = two.makeRectangle(
two.width / 2,
two.height / 2,
two.width / 4,
two.height / 4
);
rect.fill = gradient;
two.update();
var elem = two.renderer.domElement.querySelector('#' + gradient.id);
assert.equal(
elem.tagName,
'radialGradient',
'Two.RadialGradient renders as a tag.'
);
assert.equal(
parseFloat(elem.getAttribute('cx')),
0,
'The cx attribute applied properly.'
);
assert.equal(
parseFloat(elem.getAttribute('cy')),
0,
'The cy attribute applied properly.'
);
assert.equal(
parseFloat(elem.getAttribute('fx')),
0,
'The fx attribute applied properly.'
);
assert.equal(
parseFloat(elem.getAttribute('fy')),
0,
'The fy attribute applied properly.'
);
assert.equal(
parseFloat(elem.getAttribute('r')),
200,
'The r attribute applied properly.'
);
assert.equal(
elem.getAttribute('spreadMethod'),
'pad',
'The spreadMethod attribute applied properly.'
);
assert.equal(
elem.getAttribute('gradientUnits'),
'objectBoundingBox',
'The gradeintUnits attribute applied properly.'
);
assert.equal(
elem.innerHTML,
'',
'The innerHTML applied properly.'
);
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Simple)', function (assert) {
assert.expect(2);
assert.done = assert.async(1);
var two = new Two({
width: 400,
height: 400,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(path, two.width / 2, two.height / 2, 150, 100);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
var elem = two.renderer.domElement.querySelector('#' + image.id);
var id = texture.id;
assert.equal(
'url(#' + id + ')',
elem.getAttribute('fill'),
'Two.Image applied the correct texture properly.'
);
assert.equal(image.mode, 'fill', 'Two.Image uses default fill mode.');
assert.done();
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode: fit)', function (assert) {
assert.expect(2);
assert.done = assert.async(1);
var two = new Two({
width: 400,
height: 400,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
150,
100,
'fit'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
var elem = two.renderer.domElement.querySelector('#' + image.id);
var id = texture.id;
assert.equal(
'url(#' + id + ')',
elem.getAttribute('fill'),
'Two.Image applied the correct texture properly in fit mode.'
);
assert.equal(
image.mode,
'fit',
'Two.Image mode property is set correctly to fit.'
);
assert.done();
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode: fill)', function (assert) {
assert.expect(2);
assert.done = assert.async(1);
var two = new Two({
width: 400,
height: 400,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
150,
100,
'fill'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
var elem = two.renderer.domElement.querySelector('#' + image.id);
var id = texture.id;
assert.equal(
'url(#' + id + ')',
elem.getAttribute('fill'),
'Two.Image applied the correct texture properly in fill mode.'
);
assert.equal(
image.mode,
'fill',
'Two.Image mode property is set correctly to fill.'
);
assert.done();
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode: crop)', function (assert) {
assert.expect(2);
assert.done = assert.async(1);
var two = new Two({
width: 400,
height: 400,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
150,
100,
'crop'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
var elem = two.renderer.domElement.querySelector('#' + image.id);
var id = texture.id;
assert.equal(
'url(#' + id + ')',
elem.getAttribute('fill'),
'Two.Image applied the correct texture properly in crop mode.'
);
assert.equal(
image.mode,
'crop',
'Two.Image mode property is set correctly to crop.'
);
assert.done();
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode: tile)', function (assert) {
assert.expect(2);
assert.done = assert.async(1);
var two = new Two({
width: 400,
height: 400,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
150,
100,
'tile'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
var elem = two.renderer.domElement.querySelector('#' + image.id);
var id = texture.id;
assert.equal(
'url(#' + id + ')',
elem.getAttribute('fill'),
'Two.Image applied the correct texture properly in tile mode.'
);
assert.equal(
image.mode,
'tile',
'Two.Image mode property is set correctly to tile.'
);
assert.done();
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode switching)', function (assert) {
assert.expect(3);
assert.done = assert.async(1);
var two = new Two({
width: 400,
height: 400,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
150,
100,
'fit'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
var elem = two.renderer.domElement.querySelector('#' + image.id);
var id = texture.id;
assert.equal(
'url(#' + id + ')',
elem.getAttribute('fill'),
'Two.Image applied the correct texture properly in initial fit mode.'
);
assert.equal(
image.mode,
'fit',
'Two.Image initial mode property is set correctly to fit.'
);
// Change mode and verify
image.mode = 'fill';
two.update();
assert.equal(
image.mode,
'fill',
'Two.Image mode property changes correctly from fit to fill.'
);
assert.done();
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeSprite (Simple)', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
width: 400,
height: 400,
});
var path = '/tests/images/sequence/00000.png';
var sprite = two.makeSprite(path, two.width / 2, two.height / 2);
var texture = sprite.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
var elem = two.renderer.domElement.querySelector('#' + sprite.id);
var id = texture.id;
assert.equal(
'url(#' + id + ')',
elem.getAttribute('fill'),
'Two.Sprite applied the correct texture properly.'
);
assert.done();
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImageSequence', function (assert) {
assert.expect(2);
assert.done = assert.async(1);
var two = new Two({
width: 400,
height: 400,
});
var paths = [];
for (var i = 0; i < 30; i++) {
paths.push('/tests/images/sequence/' + QUnit.Utils.digits(i, 5) + '.png');
}
var sequence = two.makeImageSequence(
paths,
two.width / 2,
two.height / 2,
2
);
sequence.index = 3;
var texture = sequence.textures[sequence.index];
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
var elem = two.renderer.domElement.querySelector('#' + sequence.id);
var id = sequence.textures[sequence.index].id;
assert.equal(
'url(#' + id + ')',
elem.getAttribute('fill'),
'Two.ImageSequence applied the correct texture properly.'
);
sequence.index = 7;
id = sequence.textures[sequence.index].id;
two.update();
assert.equal(
'url(#' + id + ')',
elem.getAttribute('fill'),
'Two.ImageSequence can change index properly.'
);
assert.done();
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
two.renderer.domElement.style.cursor = 'pointer';
two.renderer.domElement.addEventListener(
'click',
function () {
if (two.playing) {
two.pause();
} else {
sequence.loop = true;
sequence.play();
two.play();
}
},
false
);
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeSprite', function (assert) {
assert.expect(2);
assert.done = assert.async(1);
var two = new Two({
width: 400,
height: 400,
});
var path = '/tests/images/spritesheet.jpg';
var sprite = two.makeSprite(
path,
two.width / 2,
two.height / 2,
4,
4,
2,
false
);
var texture = sprite.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
assert.ok(true, 'Two.Sprite created properly.');
sprite.index = 10;
two.update();
var elem = texture._renderer.elem;
var statement = [
elem.getAttribute('x'),
elem.getAttribute('y'),
elem.getAttribute('width'),
elem.getAttribute('height'),
].join(',');
assert.equal(
statement,
'-640,-640,1025,1025',
'Two.Sprite changed index properly.'
);
assert.done();
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
two.renderer.domElement.style.cursor = 'pointer';
two.renderer.domElement.addEventListener(
'click',
function () {
if (two.playing) {
two.pause();
} else {
sprite.loop = true;
sprite.play();
two.play();
}
},
false
);
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.makeGroup', function (assert) {
assert.expect(2);
var two = new Two({
width: 400,
height: 400,
});
var group = two.makeGroup();
group.className = 'hello world';
two.update();
var elem = two.renderer.domElement.querySelector('#' + group.id);
assert.equal(elem.tagName, 'g', 'Two.Group renders as a tag.');
assert.equal(
elem.getAttribute('class'),
'hello world',
'The class attribute applied properly.'
);
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Two.makeText', function (assert) {
assert.expect(17);
var two = new Two({
width: 400,
height: 400,
});
var text = two.makeText('Hello World', two.width / 2, two.height / 2);
text.fill = '#00aeff';
text.noStroke();
text.className = 'hello world';
two.update();
var elem = two.renderer.domElement.querySelector('#' + text.id);
assert.equal(elem.tagName, 'text', 'Two.Text renders as a tag.');
assert.equal(
elem.getAttribute('transform'),
'matrix(1 0 0 1 200 200)',
'The transform attribute applied properly.'
);
assert.equal(
elem.getAttribute('font-family'),
'sans-serif',
'The font-family attribute applied properly.'
);
assert.equal(
elem.getAttribute('font-size'),
'13',
'The font-size proeprty applied properly'
);
assert.equal(
elem.getAttribute('line-height'),
'17',
'The line-height attribute applied properly'
);
assert.equal(
elem.getAttribute('text-anchor'),
'middle',
'The text-anchor attribute applied properly.'
);
assert.equal(
elem.getAttribute('dominant-baseline'),
'middle',
'The dominant-baseline attribute applied properly.'
);
assert.equal(
elem.getAttribute('font-style'),
'normal',
'The font-style attribute applied properly.'
);
assert.equal(
elem.getAttribute('font-weight'),
'500',
'The font-weight attribute applied properly.'
);
assert.equal(
elem.getAttribute('text-decoration'),
'none',
'The text-decoration attribute applied properly.'
);
assert.equal(
elem.getAttribute('fill'),
'#00aeff',
'The fill attribute applied properly.'
);
assert.equal(
elem.getAttribute('stroke-width'),
'0',
'The stroke-width attribute applied properly.'
);
assert.equal(
elem.getAttribute('opacity'),
'1',
'The opacity attribute applied properly.'
);
assert.equal(
elem.getAttribute('visibility'),
'visible',
'The visibility attribute applied properly.'
);
assert.equal(
elem.getAttribute('class'),
'hello world',
'The class attribute applied properly.'
);
assert.equal(
elem.getAttribute('direction', 'ltr'),
'ltr',
'The direction attribute applied properly.'
);
assert.equal(
elem.innerHTML,
text.value,
'The value attribute applied properly.'
);
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('Styles', function (assert) {
assert.expect(9);
var two = new Two({
width: 400,
height: 400,
});
var shape = two.makeRectangle(two.width / 2, two.height / 2, 50, 50);
shape.rotation = Math.PI / 2;
shape.scale = 0.5;
shape.fill = 'lightcoral';
shape.stroke = '#333';
shape.linewidth = 10;
shape.opacity = 0.5;
shape.join = 'miter';
shape.cap = 'butt';
shape.miter = 10;
shape.className = 'pretty';
shape.closed = false;
shape.curved = true;
shape.visible = false;
shape.visible = true;
// Update Rendering
two.update();
var elem = two.renderer.domElement.querySelector('#' + shape.id);
var matrix = elem.getAttribute('transform');
assert.equal(
matrix.match(/matrix\((.*)\)/)[1],
shape._matrix.toString(),
'Two.Shape._matrix gets and sets properly.'
);
assert.equal(
elem.getAttribute('fill'),
shape.fill,
'Two.Shape.fill gets and sets properly.'
);
assert.equal(
elem.getAttribute('stroke'),
shape.stroke,
'Two.Shape.stroke gets and sets properly.'
);
assert.equal(
elem.getAttribute('stroke-linejoin'),
shape.join,
'Two.Shape.join gets and sets properly.'
);
assert.equal(
elem.getAttribute('stroke-linecap'),
shape.cap,
'Two.Shape.cap gets and sets properly.'
);
assert.equal(
elem.getAttribute('visibility'),
'visible',
'Two.Shape.visible gets and sets properly.'
);
assert.equal(
elem.getAttribute('stroke-miterlimit'),
shape.miter,
'Two.Shape.miter gets and sets properly.'
);
assert.equal(
elem.getAttribute('class'),
shape.className,
'Two.Shape.className gets and sets properly.'
);
assert.ok(
elem.getAttribute('stroke-opacity') == shape.opacity &&
elem.getAttribute('fill-opacity') == shape.opacity,
'Two.Shape.opacity gets and sets properly.'
);
QUnit.Utils.addInstanceToTest(assert.test, two);
});
})();
================================================
FILE: tests/suite/webgl.js
================================================
/**
* Tests Two.js WebGl Rendering Functionality:
*/
(function () {
QUnit.module('WebGLRenderer');
var getRatio = function (v) {
return Math.round(window.devicePixelRatio);
};
var deviceRatio = getRatio(document.createElement('canvas').getContext('2d'));
var suffix = '@' + deviceRatio + 'x.png';
QUnit.test('Two.makeLine', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 400,
height: 400,
ratio: deviceRatio,
}).appendTo(document.body);
two.makeLine(0, 0, two.width, two.height);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/line' + suffix,
two.renderer,
'Two.makeLine renders properly.'
);
});
QUnit.test('Two.makeRectangle', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 400,
height: 400,
ratio: deviceRatio,
});
two.makeRectangle(two.width / 2, two.height / 2, 100, 100);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/rectangle' + suffix,
two.renderer,
'Two.makeRectangle renders properly.'
);
});
QUnit.test('Two.makeEllipse', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 400,
height: 400,
ratio: deviceRatio,
});
two.makeEllipse(two.width / 2, two.height / 2, 100, 100);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/ellipse' + suffix,
two.renderer,
'Two.makeEllipse renders properly.'
);
});
QUnit.test('Two.makeCircle', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 400,
height: 400,
ratio: deviceRatio,
});
two.makeCircle(two.width / 2, two.height / 2, 50);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/circle' + suffix,
two.renderer,
'Two.makeCircle renders properly.'
);
});
QUnit.test('Two.makePoints', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 400,
height: 400,
ratio: 1,
});
var points = two.makePoints(200, 200, 220, 200, 180, 200);
points.size = 10;
points.noStroke();
points.fill = '#00AEFF';
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/points' + suffix,
two.renderer,
'Two.makePoints renders properly.'
);
});
QUnit.test('Two.makePath', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 400,
height: 400,
ratio: deviceRatio,
});
var amount = 20;
var points = [];
for (var i = 0; i < amount; i++) {
var pct = i / amount;
var x = pct * 300 + 50;
var y = i % 2 ? 25 : 75;
points.push(new Two.Anchor(x, y));
}
two.makePath(points, true);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/polygon' + suffix,
two.renderer,
'Two.makePath renders properly.'
);
});
QUnit.test('Two.makeCurve', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 400,
height: 400,
ratio: deviceRatio,
});
var amount = 20;
var points = [];
for (var i = 0; i < amount; i++) {
var pct = i / amount;
var x = pct * 300 + 50;
var y = i % 2 ? 25 : 75;
points.push(new Two.Anchor(x, y));
}
two.makeCurve(points, true);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/curve' + suffix,
two.renderer,
'Two.makeCurve renders properly.'
);
});
QUnit.test('Two.makeLinearGradient', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 400,
height: 400,
ratio: deviceRatio,
});
var gradient = two.makeLinearGradient(
0,
-two.height / 2,
0,
two.height / 2,
new Two.Gradient.Stop(0, 'rgb(255, 100, 100)'),
new Two.Gradient.Stop(1, 'rgb(100, 100, 255)')
);
var rect = two.makeRectangle(
two.width / 2,
two.height / 2,
two.width / 4,
two.height / 4
);
rect.fill = gradient;
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/linear-gradient' + suffix,
two.renderer,
'Two.makeLinearGradient renders properly.'
);
});
QUnit.test('Two.makeRadialGradient', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 400,
height: 400,
ratio: deviceRatio,
});
var gradient = two.makeRadialGradient(
0.5,
0.5,
2,
new Two.Gradient.Stop(0, 'rgb(255, 100, 100)'),
new Two.Gradient.Stop(1, 'rgb(100, 100, 255)')
);
var rect = two.makeRectangle(
two.width / 2,
two.height / 2,
two.width / 4,
two.height / 4
);
rect.fill = gradient;
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/radial-gradient' + suffix,
two.renderer,
'Two.makeLinearGradient renders properly.'
);
});
QUnit.test('two.makeImage (Simple)', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 200,
height: 100,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(path, two.width / 2, two.height / 2, 200, 100);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-fill' + suffix,
two.renderer,
'Two.makeImage renders properly with default fill mode.'
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode: fit)', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 200,
height: 100,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
200,
100,
'fit'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-fit' + suffix,
two.renderer,
'Two.makeImage renders properly in fit mode.'
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode: fill)', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 200,
height: 100,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
200,
100,
'fill'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-fill' + suffix,
two.renderer,
'Two.makeImage renders properly in fill mode.'
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode: crop)', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 200,
height: 100,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
200,
100,
'crop'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-crop' + suffix,
two.renderer,
'Two.makeImage renders properly in crop mode.'
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode: tile)', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 200,
height: 100,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
200,
100,
'tile'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-tile' + suffix,
two.renderer,
'Two.makeImage renders properly in tile mode.'
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode: stretch)', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 200,
height: 100,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
200,
100,
'stretch'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-stretch' + suffix,
two.renderer,
'Two.makeImage renders properly in stretch mode.'
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeImage (Mode switching)', function (assert) {
assert.expect(2);
assert.done = assert.async(2);
var two = new Two({
type: Two.Types.webgl,
width: 200,
height: 100,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var image = two.makeImage(
path,
two.width / 2,
two.height / 2,
200,
100,
'fit'
);
var texture = image.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-fit' + suffix,
two.renderer,
'Two.makeImage renders properly in initial fit mode.',
function () {
image.mode = 'fill';
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-fill' + suffix,
two.renderer,
'Two.Image changes mode properly from fit to fill.'
);
}
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
QUnit.Utils.addInstanceToTest(assert.test, two);
});
QUnit.test('two.makeSprite (Simple)', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 400,
height: 400,
ratio: deviceRatio,
});
var path = '/tests/images/sequence/00000.png';
var sprite = two.makeSprite(path, two.width / 2, two.height / 2);
var texture = sprite.texture;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/sprite-simple' + suffix,
two.renderer,
'Two.makeSprite renders properly.'
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
});
QUnit.test('two.makeImageSequence', function (assert) {
assert.expect(2);
assert.done = assert.async(2);
var two = new Two({
type: Two.Types.webgl,
width: 400,
height: 400,
ratio: deviceRatio,
});
var paths = [];
for (var i = 0; i < 30; i++) {
paths.push('/tests/images/sequence/' + QUnit.Utils.digits(i, 5) + '.png');
}
var sequence = two.makeImageSequence(
paths,
two.width / 2,
two.height / 2,
2
);
sequence.index = 3;
var texture = sequence.textures[sequence.index];
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-sequence-1' + suffix,
two.renderer,
'Two.ImageSequence applied the correct texture properly.',
function () {
sequence.index = 7;
texture = sequence.textures[sequence.index];
texture._flagImage = true;
texture.bind(Two.Events.Types.load, function () {
texture.unbind(Two.Events.Types.load);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-sequence-2' + suffix,
two.renderer,
'Two.ImageSequence can change index properly.'
);
});
texture._update();
}
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
two.renderer.domElement.style.cursor = 'pointer';
two.renderer.domElement.addEventListener(
'click',
function () {
if (two.playing) {
two.pause();
} else {
sequence.loop = true;
sequence.play();
two.play();
}
},
false
);
});
QUnit.test('two.makeSprite', function (assert) {
assert.expect(2);
assert.done = assert.async(2);
var two = new Two({
type: Two.Types.webgl,
width: 400,
height: 400,
ratio: deviceRatio,
});
var path = '/tests/images/spritesheet.jpg';
var sprite = two.makeSprite(
path,
two.width / 2,
two.height / 2,
4,
4,
2,
false
);
var texture = sprite.texture;
sprite.index = 3;
var loaded = function () {
texture.unbind(Two.Events.Types.load, loaded);
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-sequence-1' + suffix,
two.renderer,
'Two.makeSprite renders properly.',
function () {
sprite.index = 7;
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/image-sequence-2' + suffix,
two.renderer,
'Two.Sprite changed index properly.'
);
}
);
};
texture.bind(Two.Events.Types.load, loaded);
texture._update();
two.renderer.domElement.style.cursor = 'pointer';
two.renderer.domElement.addEventListener(
'click',
function () {
if (two.playing) {
two.pause();
} else {
sprite.loop = true;
sprite.play();
two.play();
}
},
false
);
});
QUnit.test('Two.makeText', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 400,
height: 400,
ratio: deviceRatio,
});
var text = two.makeText('Hello World', two.width / 2, two.height / 2);
text.fill = '#00aeff';
text.noStroke();
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/text' + suffix,
two.renderer,
'Two.makeText renders properly.'
);
});
QUnit.test('Styles', function (assert) {
assert.expect(1);
assert.done = assert.async(1);
var two = new Two({
type: Two.Types.webgl,
width: 400,
height: 400,
ratio: deviceRatio,
});
var shape = two.makeRectangle(two.width / 2, two.height / 2, 50, 50);
shape.rotation = Math.PI / 2;
shape.scale = 0.5;
shape.fill = 'lightcoral';
shape.stroke = '#333';
shape.linewidth = 10;
shape.opacity = 0.5;
shape.join = 'miter';
shape.cap = 'butt';
shape.miter = 10;
shape.closed = false;
shape.curved = true;
shape.visible = false;
shape.visible = true;
two.update();
QUnit.Utils.compare.call(
assert,
'./images/canvas/styles' + suffix,
two.renderer,
'Styles render properly.'
);
});
})();
================================================
FILE: tests/typescript/index.js
================================================
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var two_js_1 = require("two.js");
var two = new two_js_1.default({
fullscreen: true,
autostart: false,
});
var path;
path = two_js_1.default.Circle.fromObject({
radius: 5,
stroke: 'blue',
fill: 'yellow',
// position: new Two.Vector(),
rotation: 5,
translation: { x: 0, y: 0 },
});
path = new two_js_1.default.Line(5, 5, 10, 10);
two.add(path);
var group = new two_js_1.default.Group(path);
two.add(group);
path = new two_js_1.default.Circle(3, 3, 10);
group.add(path);
path = new two_js_1.default.Rectangle(25, 25, 5, 5);
group.add(path);
path = new two_js_1.default.Polygon(30, 30, 5, 7);
group.add(path);
path = new two_js_1.default.ArcSegment(5, 25, 5, 10, 0, Math.PI);
group.add(path);
path = new two_js_1.default.Ellipse(5, 30, 10, 5);
group.add(path);
path = new two_js_1.default.Star(30, 5, 10, 5);
group.add(path);
path = new two_js_1.default.Points([new two_js_1.default.Anchor(-2, 0), new two_js_1.default.Anchor(2, 0)]);
group.add(path);
path = new two_js_1.default.Path([new two_js_1.default.Anchor(-1, 0), new two_js_1.default.Anchor(1, 0)]);
path.closed = false;
path.automatic = true;
path.position.set(two.width / 2, two.height / 2);
path.scale = new two_js_1.default.Vector(1, 1);
group.add(path);
two.appendTo(document.body);
================================================
FILE: tests/typescript/index.ts
================================================
import Two from 'two.js';
const two = new Two({
fullscreen: true,
autostart: false,
});
let path;
path = Two.Circle.fromObject({
radius: 5,
stroke: 'blue',
fill: 'yellow',
// position: new Two.Vector(),
rotation: 5,
translation: { x: 0, y: 0 },
});
path = new Two.Line(5, 5, 10, 10);
two.add(path);
const group = new Two.Group(path);
two.add(group);
path = new Two.Circle(3, 3, 10);
group.add(path);
path = new Two.Rectangle(25, 25, 5, 5);
group.add(path);
path = new Two.Polygon(30, 30, 5, 7);
group.add(path);
path = new Two.ArcSegment(5, 25, 5, 10, 0, Math.PI);
group.add(path);
path = new Two.Ellipse(5, 30, 10, 5);
group.add(path);
path = new Two.Star(30, 5, 10, 5);
group.add(path);
path = new Two.Points([new Two.Anchor(-2, 0), new Two.Anchor(2, 0)]);
group.add(path);
path = new Two.Path([new Two.Anchor(-1, 0), new Two.Anchor(1, 0)]);
path.closed = false;
path.automatic = true;
path.position.set(two.width / 2, two.height / 2);
path.scale = new Two.Vector(1, 1);
group.add(path);
two.appendTo(document.body);
================================================
FILE: tests/typescript/package.json
================================================
{
"name": "typescript",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"two.js": "file:../../",
"typescript": "^5.2.2"
}
}
================================================
FILE: utils/INSTRUCTIONS.md
================================================
# TypeScript Types Declaration Build Instructions
1. Remove all @extends commands from `/src` and `/extras`
2. Run `npm run types`
---
In output code `types.d.ts` do:
3. Remove all `Two.*` references — e.g: `Two.Vector` -> `Vector`
4. Remove all `typeof` references _except_ for static properties of Two — e.g: `Two.Group`
5. Remove all `'.js'` import references
6. Change `"src/two"` to `two.js`
7. Change all `"src/` references to `"two.js/src/`
8. Change all `"extras/"` references to `"two.js/extras/`
9. Fix all `constructor` methods to have proper argument ingestion
- Includes: `Two.Rectangle`, `Two.Circle`, `Two.Ellipse`, `Two.Line`, and `Two.Polygon`
10. Fix private property assignment on `Two.Group`. e.g: `_stroke` -> `stroke`
- Includes `Two.Text`
11. Add optional overloaded functions to `Two.makeCurve` and `Two.makePath` for `closed` attribute
- And other functions with `...` argument syntax
Things to do to improve TypeScript Types:
1. Add default values to all methods (including constructors) to clarify what is mandatory and what is optional.
2. Figure out how to handle both ingestion of single argument Array's and full arguments as arrays.
================================================
FILE: utils/build.js
================================================
const esbuild = require('esbuild');
const fs = require('fs');
const path = require('path');
const _ = require('underscore');
const gzip = require('gzip-size');
const publishDateString = new Date().toISOString();
const config = getJSON('package');
const paths = {
entry: path.resolve(__dirname, '../src/two.js'),
umd: path.resolve(__dirname, '../build/two.js'),
esm: path.resolve(__dirname, '../build/two.module.js'),
min: path.resolve(__dirname, '../build/two.min.js'),
license: path.resolve(__dirname, '../LICENSE'),
};
async function buildModules() {
esbuild.buildSync({
entryPoints: [paths.entry],
outfile: paths.umd,
bundle: true,
minify: false,
format: 'iife',
globalName: 'Two',
});
esbuild.buildSync({
entryPoints: [paths.entry],
outfile: paths.esm,
bundle: true,
target: 'es6',
format: 'esm',
});
esbuild.buildSync({
entryPoints: [paths.entry],
outfile: paths.min,
bundle: true,
minify: true,
format: 'iife',
globalName: 'Two',
});
const license = await fs.promises.readFile(paths.license, {
encoding: 'utf-8',
});
const licenseComment = ['/*', license.trim(), '*/'].join('\n');
const umdOutput = await fs.promises.readFile(paths.umd);
const esmOutput = await fs.promises.readFile(paths.esm);
const minOutput = await fs.promises.readFile(paths.min);
const moduleExports = `(function(){if(typeof exports==='object'&&typeof module!=='undefined'){module.exports=Two}})()`;
return Promise.all([
fs.promises.writeFile(
paths.umd,
[licenseComment, template(umdOutput, true), moduleExports].join('\n')
),
fs.promises.writeFile(
paths.esm,
[licenseComment, template(esmOutput, false)].join('\n')
),
fs.promises.writeFile(
paths.min,
[licenseComment, template(minOutput, true), moduleExports].join('\n')
),
]);
}
function template(buffer, isExposed) {
const code = buffer.toString();
const generate = _.template(code);
let result = generate({
version: config.version,
publishDate: publishDateString,
});
if (isExposed) {
result = result.replace(/\}\)\(\);\s*$/, '})().default;');
}
return result;
}
function publishModule() {
let size;
const result = {};
size = getFileSize('two.js');
result.development = formatFileSize(size);
size = getFileSize('two.min.js');
result.production = formatFileSize(size);
const contents = JSON.stringify(result);
const outputPath = path.resolve(__dirname, './file-sizes.json');
return fs.promises.writeFile(outputPath, contents);
}
function getFileSize(filename) {
const file = fs.readFileSync(path.resolve(__dirname, '../build', filename));
return gzip.sync(file);
}
function formatFileSize(v) {
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
let iterations = 0;
while (v > 1000) {
v *= 0.001;
iterations++;
}
return [Math.round(v), sizes[iterations]].join('');
}
async function build() {
let startTime, elapsed;
startTime = Date.now();
try {
await buildModules();
} catch (error) {
console.log(error);
}
elapsed = Date.now() - startTime;
console.log('Built and minified Two.js:', elapsed / 1000, 'seconds');
elapsed = Date.now() - startTime;
try {
await publishModule();
} catch (error) {
console.log(error);
}
console.log(
'Published additional statistics to wiki:',
elapsed / 1000,
'seconds'
);
}
function getJSON(filename) {
var file = fs.readFileSync(path.resolve(__dirname, '..', `${filename}.json`));
return JSON.parse(file);
}
build();
================================================
FILE: utils/docs.template
================================================
---
title: <%= root.longname %>
pageClass: docs
lang: en-US
---
# <%= root.longname %>
<% if (root.augments) { %>
================================================
FILE: wiki/README.md
================================================
---
title: Two.js • Homepage
lang: en-US
---
# Two.js
A two-dimensional drawing api geared towards modern web browsers. It is renderer agnostic enabling the same api to draw in multiple contexts: svg, canvas, and webgl.
::: tip
Prior to v0.7.0-alpha.1 Two.js requires Underscore.js and Backbone.js Events. If you're already loading these files elsewhere then you can build the project yourself and get the file size even smaller. For more information on custom builds check out the source on github.
:::
Node.js Version with npm:
```
npm install --save two.js@latest
```
## Overview
* #### Focus on Vector Shapes
Two.js is deeply inspired by flat [motion graphics](http://en.wikipedia.org/wiki/Motion_graphics). As a result, two.js aims to make the creation and animation of flat shapes easier and more concise.
* #### Scenegraph
At its core two.js relies on a [scenegraph](http://en.wikipedia.org/wiki/Scene_graph). This means that when you draw or create an object (a Two.Path or Two.Group), two actually stores and remembers that. After you make the object you can apply any number of operations to it — e.g: rotation, position, scale, etc..
* #### Animation Loop
Two.js has a built in animation loop. It is simple in nature and can be automated or paired with another animation library. For more information check out the [examples](/examples/).
* #### SVG Interpreter
Two.js features a [Scalable Vector Graphics](http://en.wikipedia.org/wiki/Scalable_Vector_Graphics) Interpreter. This means developers and designers alike can create SVG elements in commercial applications like [Adobe Illustrator](http://www.adobe.com/products/illustrator) and bring them into your two.js scene. For more information check out the [examples](/examples/).
* #### Friends with Bitmap Imagery
Despite its early focus on easing vector shape creation and animation, Two.js offers many easy-to-use features to handle and render bitmap images. Easily load single images, sprite sheets, and image sequences with just a few method calls.
## Basic Usage
In order to start any of these demos you'll want to [download](#download) two.js and add it to your HTML document. Once downloaded add this tag to the `` of your document: ``. When you visit the page, you should be able to open up the console and type `Two`. If this returns a function (and not an error) then you're ready to begin!
## Drawing Your First Shapes
Before we get into all the fancy animating it's good to get a feel for how to make shapes in two.js. In order to do this we need to have an instance of two. This sets up a dom element that contains either an svg or canvas element to add to the webpage. The two object has a scene which holds all shapes as well as methods for creating shapes.
::: tip
For a list of all properties and construction parameters check out the [documentation](./docs/).
:::
// Make an instance of two and place it on the page.
var params = { fullscreen: true };
var elem = document.body;
var two = new Two(params).appendTo(elem);
// Two.js has convenient methods to make shapes and insert them into the scene.
var radius = 50;
var x = two.width * 0.5;
var y = two.height * 0.5 - radius * 1.25;
var circle = two.makeCircle(x, y, radius);
y = two.height * 0.5 + radius * 1.25;
var width = 100;
var height = 100;
var rect = two.makeRectangle(x, y, width, height);
// The object returned has many stylable properties:
circle.fill = '#FF8000';
// And accepts all valid CSS color:
circle.stroke = 'orangered';
circle.linewidth = 5;
rect.fill = 'rgb(0, 200, 255)';
rect.opacity = 0.75;
rect.noStroke();
// Don’t forget to tell two to draw everything to the screen
two.update();
## Shapes and Groups
Adding shapes to groups makes managing multiple shapes easier and more sane. Group's provide an easy way to move your content through `position`, `rotation`, and `scale`. These operations emit from the coordinate space `(0, 0)`. In the example below we can see that the initial orientation of the circle and rectangle changed from the first example. These shapes are oriented around `(0, 0)`, which allows us to transform the group around the centeroid of the shapes. In addition Group's styling operations trickle down and apply to each shape.
var params = { fullscreen: true }
var elem = document.body;
var two = new Two(params).appendTo(elem);
var circle = two.makeCircle(-70, 0, 50);
var rect = two.makeRectangle(70, 0, 100, 100);
circle.fill = '#FF8000';
circle.stroke = 'orangered';
rect.fill = 'rgba(0, 200, 255, 0.75)';
rect.stroke = '#1C75BC';
// Groups can take an array of shapes and/or groups.
var group = two.makeGroup(circle, rect);
// And have position, rotation, scale like all shapes.
group.position.set(two.width / 2, two.height / 2);
group.rotation = Math.PI;
group.scale = 0.75;
// You can also set the same properties a shape have.
group.linewidth = 7;
two.update();
## Adding Motion
Finally, let's add some motion to our shapes. So far the examples use `two.update();` to draw content to the screen. The instance of two.js has two particular methods for animation. The first is `two.play();` which calls `two.update();` at 60 frames-per-second. This rate, however, will slowdown if there's too much content to render per frame.
The second method is `two.bind();` This method takes a string as its first parameter indicating what event to listen to and a function as its second argument delineating what to do when the event described in the first parameter happens. To sync a function with the animation loop simply invoke `two.bind('update', referenceToFunction);` as outlined below:
var params = { fullscreen: true };
var elem = document.body;
var two = new Two(params).appendTo(elem);
var circle = two.makeCircle(-70, 0, 50);
var rect = two.makeRectangle(70, 0, 100, 100);
circle.fill = '#FF8000';
rect.fill = 'rgba(0, 200, 255, 0.75)';
var cx = two.width * 0.5;
var cy = two.height * 0.5;
var group = two.makeGroup(circle, rect);
group.position.set(cx, cy);
group.scale = 0;
group.noStroke();
// Bind a function to scale and rotate the group to the animation loop.
two.bind('update', update);
// Finally, start the animation loop
two.play();
function update(frameCount) {
// This code is called every time two.update() is called.
if (group.scale > 0.9999) {
group.scale = group.rotation = 0;
}
var t = (1 - group.scale) * 0.04;
group.scale += t;
group.rotation += t * 4 * Math.PI;
}
### Next Steps
Now that you got a quick glimpse into some of the functionality two.js offers, check out the [official](/examples/#official-examples) and [community](/examples/#community-examples) examples to see what else you can do. These examples range from showing off specific features of the library to using the library in other environments, like [React](/examples/#react) and [Angular](/examples/#angular).
Looking for more information on a specific property? Then head over to the [documentation](/docs/two/) which outlines all of the library's public features.
Haven't found what you're looking for? Then ask a question on our [GitHub](https://github.com/jonobr1/two.js/issues/new?assignees=&labels=question&template=question.md&title=%5BQuestion%5D) page.
---
#### Project Credits
Two.js is dependency free, but its creation would not have been possible without these great contributions to the JavaScript ecosystem:
================================================
FILE: wiki/change-log/README.md
================================================
================================================
FILE: wiki/changelog/README.md
================================================
---
pageClass: change-log
sidebarDepth: 3
title: Two.js Changelog
lang: en-US
---
# Changelog
All notable changes to this project will be documented in this file. The format is inspired by [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).
## Nightly
- Separated type signatures into their own respective files
## December 22, 2025 v0.8.23
Dec 22, 2025
- Made `Two.Event` private getter more relaxed
- Added `Two.Shape.dispose`
- Added test suite for `dispose` methods
## December 3, 2025 v0.8.22
Dec 3, 2025
- Improved `Two.Path.subdivide` to preserve curves (by filling out `Two.Anchor.controls` property)
- Added `Two.Path.smooth` command
- Added `Two.Path.contains`, `Two.Group.contains`, and `Two.Shape.contains` method for hit testing
- Added `Two.getShapesAtPoint` and `Two.Group.getShapesAtPoint` with visibility/filter options
## October 1, 2025 v0.8.21
Oct 1, 2025
- Added `Two.Path.strokeAttenuation`, `Two.Group.strokeAttenuation`, `Two.Points.strokeAttenuation`, `Two.Text.strokeAttenuation`
- Improved type signatures on all `fromObject` methods for better hinting
- Improved precision for curve details
- Added Privacy Policy
- Added SECURITY.md
- Added Incident Response Plan
- Enabled Code Scanning
- Added more tests for security vulnerabilities
## September 8, 2025 v0.8.20
Sep 8, 2025
- Fixed `Two.Element.copy` to fail gracefully
- Added `Two.Image` as a new shape that inherits `Two.Rectangle`
- Expand `Two.RoundedRectangle` constructor to accept `Two.Vector` as the radius
- Added `Two.Element.dispose`, `Two.Path.dispose`, `Two.Group.dispose`, `Two.Text.dispose`, `Two.Points.dispose`, `Two.ImageSequence.dispose`, `Two.Sprite.dispose`
- Improved `Two.release` to account for renderer specific deallocations
- Improved `Two.ImageSequence` to take all optional arguments on instantiation
- Fixed command setting on `Two.Line.left` and `Two.Line.right`
## June 27, 2025 v0.8.19
Jun 27, 2025
- Improved TypeScript types for `Two.Path.dashes`, `Two.Points.dashes`, `Two.Text.dashes`
- Improved TypeScript types for the `Two.Element.renderer` property
- Added `Two.Element.renderer.onBeforeRender` and `Two.Element.renderer.onAfterRender` for higher context manipulation
## April 8, 2025 v0.8.18
Apr 8, 2025
- Improve TypeScript types with correct inheritance and no errors
- Fix `Two.release` in order to check for object existence on `fill` and `stroke` properties
## Mar 17, 2025 v0.8.17
Mar 17, 2025
- Improved and updated TypeScript types based on public / private properties and new latest Two.js features
- Added `Two.Gradient.dispose` and `Two.Texture.dispose` to properly remove effects from scenegraphs
- Changed name `Two.nextFrameID` to `Two.NextFrameId`
- Improved `Two.release` method to include detaching an object's effects (e.g: `Two.Texture`, `Two.Gradient`)
- Fixed order of operations on `Two.Shape` transformations
## Feb 13, 2025 v0.8.16
Feb 13, 2025
- Improved TypeScript types (albeit raising errors because of how expressive Two.js / JavaScript is)
- Move `Two.Utils.shim` to `Two.Utils.polyfill`. Shim exists but is internal specific to remove circular dependencies
- Add `Two.CanvasRenderer` compatiblity with [skia-canvas](https://skia-canvas.org/)
- `Two.Path.subdivide` accurately amends the final vertices in the updated list
- `Two.ZUI.reset` updates the surfaces to be reflect reset orientation
- `Two.Group.getBoundingClientRect(shallow)` correctly infers min and max values to calculate dimensions correctly
## Dec 31, 2024 v0.8.15
Dec 31, 2024
- Made `CanvasPolyfill` object and merged it into `Two.Utils` to remove circular dependencies
- Made `Two.Collection._events` and `Two.Children.ids` not enumerated for more seamless invocation of `Array` methods like `Array.filter` and `Object` methods like `Object.keys`
- Added static method `fromObject` to all `Two.Element` and descendant classes
- Added method `copy` to all `Two.Element` and descendant classes
- Added tests for `toObject` methods
- Improved TypeScript types for argument relation
- `Two.WebGLRenderer` correctly handles negative scale values
- `Two.Path.noStroke`, `Two.Text.noStroke`, and derivative methods now include setting both the stroke color to `"none"` and the line width to 0
- Fixed `Two.Arc` flag behavior for `Two.Arc.width` and `Two.Arc.height` properties
## Jun 10, 2024 v0.8.14
Jun 10, 2024
- Made `objectBoundingBox` application consistent accross renderers
- Patched texture scaling in `Two.SVGRenderer`
- Updated Typescript definitions [@kevin-nano](https://github.com/kevin-nano)
## Feb 22, 2024 v0.8.13
Feb 22, 2024
- Added `Two.Text.direction` for rendering text either left to right or right to left
- Improved `Two.WebGLRenderer` texture crispness / aliasing
- Improved consistency of `Two.Text.baseline` rendering across all renderers
## Oct 16, 2023 v0.8.12
Oct 16, 2023
- Added `"no-referrer"` policy to image requests
- Updated extras to be compatible with `Two.Matrix` API changes
- `Two.Path.noFill()` yields `"none"` instead of `"transparent"`
- `Two.Path.noStroke()` yields `"none"` instead of `"transparent"`
- `Two.Text.noFill()` yields `"none"` instead of `"transparent"`
- `Two.Text.noStroke()` yields `"none"` instead of `"transparent"`
- `Two.Points.noFill()` yields `"none"` instead of `"transparent"`
- `Two.Points.noStroke()` yields `"none"` instead of `"transparent"`
## Aug 7, 2023 v0.8.11
Aug 7, 2023
- Fixed getters on `Two.Anchor.left` and `Two.Anchor.right` [@eatgrass](https://github.com/eatgrass)
- Improved `Two.Path.getBoundingClientRect`, `Two.Group.getBoundingClientRect`, and `Two.Text.getBoundingClientRect` to correctly handle projected points that are rotated by matrices
- Improved `types.d.ts` for `Two.Group`
- `Two.Text` rendered to SVG directly instead of ``
- `Two.SVGRenderer` disposes of unused effects from ``
- Override `Two.Collection.map` method to be more explicit
- Improved `types.d.ts` for renderers and base `Two` class
## Jun 9, 2022 v0.8.10
Jun 9, 2022
- Added `Two.Arc` to `/extras` directory
- Made `/extras/js/zui.js` ES6 compliant
- Removed `/extras` modules from generated documentation
- Added method documentation to `Two.ZUI`
- Improved `types.d.ts`
- Fixed Commonjs imports from `package.json`
## May 9, 2022 v0.8.7
May 9, 2022
- Fixed documentation links
- Made `Two.Group` and `Two.Text` properties public in TypeScript types
- Added overloaded methods to TypeScript types
- Fixed `Two.Path.ending` discrepancies when `Two.Path.curved = true`
## Mar 29, 2022 v0.8.5
Mar 29, 2022
- Added `Two.Gradient` parent parameter to `Two.Stop.clone`
- **_Breaking_**: Added export maps so extras are imported like so `import { ZUI } from 'two.js/extras/zui.js'`
## Jan 29, 2022 v0.8.3
Jan 29, 2022
- Improved `Two.Element.className` flagging and `classList` construction
- Manually amended `types.d.ts` for better TypeScript developing
## Jan 19, 2022 v0.8.2
Jan 21, 2022
- Removed `#` private declared variables for better EcmaScript compatibility
- Fixed `Two.Path.begining` and `Two.Path.ending` interpolation when `Two.Anchor.relative = false`
- Added `Two.Utils.read.path(string)` which allows for string interpretation of an SVG path's `d` attribute
- Added `Two.Shape.worldMatrix`
## Jan 10, 2022 v0.8.0
Jan 10, 2022
- Added `Two.Element` as an EcmaScript 6 class
- Converted `Two.ImageSequence` to EcmaScript 6 class
- Converted `Two.Sprite` to EcmaScript 6 class
- Converted `Two.Texture` to EcmaScript 6 class
- Converted `Two.Stop` to EcmaScript 6 class
- Converted `Two.RadialGradient` to EcmaScript 6 class
- Converted `Two.LinearGradient` to EcmaScript 6 class
- Converted `Two.Gradient` to EcmaScript 6 class
- Converted `Two.Star` to EcmaScript 6 class
- Converted `Two.RoundedRectangle` to EcmaScript 6 class
- Converted `Two.Rectangle` to EcmaScript 6 class
- Converted `Two.Polygon` to EcmaScript 6 class
- Converted `Two.Points` to EcmaScript 6 class
- Converted `Two.Line` to EcmaScript 6 class
- Converted `Two.Ellipse` to EcmaScript 6 class
- Converted `Two.Circle` to EcmaScript 6 class
- Converted `Two.ArcSegment` to EcmaScript 6 class
- Converted `Two.Path` to EcmaScript 6 class
- Converted `Two.Group` to EcmaScript 6 class
- Converted `Two.Text` to EcmaScript 6 class
- Converted `Two.Shape` to EcmaScript 6 class
- Converted `Two.Matrix` to EcmaScript 6 class
- Converted `Two.Registry` to EcmaScript 6 class
- Converted `Two.Children` to EcmaScript 6 class
- Converted `Two.Collection` to EcmaScript 6 class
- Converted `Two.Events` to EcmaScript 6 class
- Converted `Two.Anchor` to EcmaScript 6 class
- Converted `Two.Vector` to EcmaScript 6 class
- `Two.interpret` appropriately inherits classes to `` tags [@elShiaLabeouf](https://github.com/elShiaLabeouf)
- `Two.interpret` inherits classes and `data-*` attributes [@elShiaLabeouf](https://github.com/elShiaLabeouf)
## Dec 15, 2021 v0.7.13
Dec 15, 2021
- Improved SVG gradient interpretation
- `Two.interpret` can properly unwrap CSS `url()` commands
- Added `Two.Gradient.units` and respected in all renderers
- Default units space for `Two.Gradient` is `objectBoundingBox`
- Removed destructive attribute assignments in `Two.interpret`
- Interpreted gradients are reused as ``
## Nov 24, 2021 v0.7.12
Nov 24, 2021
- Made mask removal possible on `Two.Group`s, `Two.Path`s, and `Two.Points`'
- Simplified `Two.Points.vertices` management for renderers improving adaptability
## Nov 20, 2021 v0.7.10
Nov 20, 2021
- Fixed empty vertices issue on `Two.Points`
## Nov 11, 2021 v0.7.9
Nov 11, 2021
- Made `Two.load` and `Two.interpret` consistently apply SVG `viewBox`
- Made `Two.Rectangle.origin` act as inverse anchor positioning
- Made `Two.Path.toObject` objectify `fill` and `stroke` attributes that are `Two.Texture` and `Two.Gradient`
- Improved reassigned id selection on interpreted SVG elements
- Improved `Two.load` group duplication
- Added `Two.Points` and `Two.makePoints` to all renderers
- Made `Two.release` mimic behavior of `Two.remove`
## Jul 13, 2021 v0.7.8
Jul 13, 2021
- Made `types.d.ts` a shim of a module so that it can at least be loaded in TypeScript projects while the ES6 class declaration is still work-in-progress
## Jul 10, 2021 v0.7.6
Jul 10, 2021
- Fixed `id` setting on all objects of Two.js
- Fixed `Two.ArcSegment.clone` outer radius issue
- Standardized vertex generation in higher order primitives
- Type declaration improvements
- Improved `Two.Group.center`, `Two.Group.corner`, `Two.Path.center`, `Two.Path.corner`
- Improved SVG gradient interpretation
- Added `` interpretation
- Added `Two.Path.mask` and `Two.Text.mask` properties
- Standardized `Texture.id` to be compliant with other `Two.Shape.id`
- Added ability to interpret `` and `` SVG elements
## Apr 23, 2021 v0.7.5
Apr 23, 2021
- Improved `Two.Circle.beginning` & `Two.Circle.ending` behavior
- Improved `fitted` logic on instances of `Two`
- Improved `Two.Children` and `Two.Collection` instantiation versatility
- Improved `Two.Group.getBoundingClientRect`, `Two.Path.getBoundingClientRect`, and `Two.Text.getBoundingClientRect`
- Improved higher level shapes', like `Two.Circle`, ability to handle zeroed out arguments
## Apr 2, 2021 v0.7.4
Apr 2, 2021
- Improved Typescript type declarations
- Successfully export `extras/` directory in both JavaScript and JavaScript Module format
- Added `Two.Shape.skewX` and `Two.Shape.skewY` [@gburlet](https://github.com/gburlet)
## Mar 26, 2021 v0.7.3
Mar 26, 2021
- Exposed `Two.Shape.renderer` for easier access to underlying SVG elements
- Added Typescript declaration types to Two.js
- Made `Two.Group` listen to `Two.Shape.id` changes through new observable property and flags
- Improved `Two.interpret` performance and functionality through improved regular expressions [@adroitwhiz](https://github.com/adroitwhiz)
- `Two.Group.visible` is a property held on the group and does not trickle down to children
- Improved scientific notation interpretation in SVG elements
- `Two.load` appends a hidden DOM element to the document's `` so that `getComputedStyles` works more consistently with `Two.interpret`
## Jan 12, 2021 v0.7.1
Jan 12, 2021
- Added `Two.AutoCalculateImportedMatrices` to control different ways of importing SVG transformations
- Fixed `Two.Text.className` application
- Added basic text support in `Two.interpret`
- Exposed `Two.fit` for external use
- Added a `fitted` argument to the `Two` constructor to make instance adaptively match the size of its parent element
- Simplified `Two.release` method
- Added support for scientific notation in `Two.interpret`
- Added `Two.Text.decoration` styling to `Two.CanvasRenderer` and `Two.WebGLRenderer`
- Increased precision on `Two.Utils.toFixed` for Firefox transformation engine performance increase
- Added support to import `` tags from SVG interpretation [@eulertour](https://github.com/eulertour)
- Removed extraneous underscore calls from `Two.Utils` [@adroitwhiz](https://github.com/adroitwhiz)
- Ensured `Two.interpret` respects the `add` parameter to be added (or not) to the current instance
- Modularized Two.js classes / files [@adroitwhiz](https://github.com/adroitwhiz)
- Made Two.js compatible with `import` modules [@adroitwhiz](https://github.com/adroitwhiz)
- Added wiki as [VuePress](https://vuepress.vuejs.org/) project
## Jan 22, 2020 v0.7.0
Jan 22, 2020
- Exposed `resolution` parameter in `Two.makeCircle` and `Two.makeEllipse` as the final parameter
- Made `Two.Circle` and `Two.Ellipse` recalculate controls points on `_update` and made vertex amounts cyclical [@adroitwhiz](https://github.com/adroitwhiz)
- Added [ESLint](https://eslint.org/) scripts to development environment [@adroitwhiz](https://github.com/adroitwhiz)
- Improve performance of WebGLRenderer by leveraging uniform to construct plane and removing duplicate render calls [@adroitwhiz](https://github.com/adroitwhiz)
- Unpacked `scale` objects in WebGLRenderer [@adroitwhiz](https://github.com/adroitwhiz)
- Removed unnecessary `gl.colorMask` calls [@adroitwhiz](https://github.com/adroitwhiz)
- Removed `Two.Utils.toFixed` calls on Canvas and WebGLRenderers [@adroitwhiz](https://github.com/adroitwhiz)
- `Two.Shape.clone` clones `Two.Shape.matrix` when `Two.Shape.matrix.manual` is set to `true`
- Improved `Two.Group.mask` rendering in `Two.WebGLRenderer`
- Fixed `Two.WebGLRenderer.setSize` recursive loop error
- Connected `Two.Shape.className` to `Two.Shape.classList` for searching and class assignment in SVG elements
- Performance improvements on canvas HTML5 styles [@brandonheyer](https://github.com/brandonheyer)
- Added trickle down styling to `Two.Group.closed`, `Two.Group.curved`, and `Two.Group.automatic`
- Check for Duplicity on `Two.Group.add`
- Accounted for offset positions in `Two.Path.center` and `Two.Group.center` methods
- Exposed `Two.Shape.matrix` as a publicly accessible property
- Removed `Two.Utils.deltaTransformPoint` and patched `Two.Utils.decomposeMatrix` to more accurately parse matrices
- Added support for various position inclusion of gradients and other effects in `Two.interpret`
- Improved `Two.Utils.applySvgAttributes` rotation from SVG interpretation
- Added `Two.makeArrow` for Simple Triangular Tipped Arrows [@mike168m](https://github.com/mike168m)
- Improved `Two.Matrix` efficiency of calculations
- Added `Two.Path.dashes.offset` and `Two.Text.dashes.offset` properties for animating dashed strokes in all renderers
- Fixed `Two.Path.corner` method to not be additive on successive invocations
- Split `Two.Matrix.toArray` into two different functions. One for 2D transforms and one for a plain object (JSON) representation
- Added `Two.Matrix.toTransformArray` intended for 2D transformation use internally
- `WebGLRenderer` more robustly supports displaying bitmap content
- Added `` attributes to be inherited by children in SVG interpretation
- Added `offscreenElement` as an option when constructing WebGL Renderers for WebWorker compatibility
- Added `Two.Shape.position` accessor to `Two.Shape.translation` for ease of use with [matter.js](http://brm.io/matter-js/)
- Added `Two.Path.dashes` and `Two.Text.dashes` support to WebGL and Canvas Renderers
## Dec 8, 2018 v0.7.0-beta.3
Dec 8, 2018
- Canvas Renderer supports dashed and non dashed paths
- Enforce `Two.Rectangle` has four `vertices`
- Fixed `Two.Path.closed` on latest `ending` calculations
## Nov 18, 2018 v0.7.0-beta.2
Nov 18, 2018
- Updated Two.js compatibility with webpack and node-canvas 2.0.0+
## Nov 3, 2018 v0.7.0-beta.1
Nov 3, 2018
- Altered `Two.Path.clone` and `Two.Text.clone` to use references where possible and to `_update()` on return
- Improved multi-decimal and arc SVG interpretation
- Added `Two.Commands.arc` for better arc rendering across all renderers
- `Two.Path` and `Two.Text` now have `dashes` property to define stroke dashing behavior [@danvanorden](https://github.com/danvanorden)
- `Two.Vector` arithmetic methods made more consistent — still need to improve performance
- `Two.Path.vertices` will not clone vectors, improving developer clarity
- Two.js clone methods do not force adding to a parent
- `Two.ImageSequence`, `Two.Sprite`, and `Two.Rectangle` have `origin` properties for offset rendering
- `Two.Group.getBoundingClientRect` will pass-through on effects instead of break
- `Two.interpret` apply SVG node `style` attributes to paths. Inherits from groups ~~and infers SVG `viewBox` attribute against Two.js instance~~
- `Two.interpret` improves multi-decimal formatted `d` attributes
- `Two.ZUI` added through the new `/extras` folder
- `Two.Text.getBoundingClientRect` now returns an estimated bounding box object
- `Two.interpret` properly assigns back calculated `Z` coordinates
- `Two.load` now immediately returns a `Two.Group` for use without callbacks if desired
- Added `Two.Group.length` to return the calculated length of all child paths
- `Two.Group.beginning` and `Two.Group.ending` calculate based on child `Two.Path`s for intuitive grouped animating
- Added `Two.Utils.shim` to properly handle `canvas` and `image` element spoofing in headless environments
- Improved conformance between primitive shapes
- `Two.Path.getBoundingClientRect` considers control points from bezier curves
- `Two.Path.beginning` and `Two.Path.ending` calculate based on distance increasing accuracy for animation, but also performance load
- Moved `Two.Path._vertices` underlying to list of rendered points to `Two.Path._renderer.vertices`
- Improved accuracy of `Two.Path.ending` and `Two.Path.beginning` on open paths
- Added specific `clone` method to `Two.ArcSegment`, `Two.Circle`, `Two.Ellipse`, `Two.Polygon`, `Two.Rectangle`, `Two.RoundedRectangle`, and `Two.Star` primitives
- Added ability to read `viewBox` property from root SVG node in `Two.interpret`
- Added more reliable transform getter in `Two.interpret`
- Added `rx` and `ry` property reading on `Two.Utils.read.rect`
- Added `Two.Utils.read['rounded-rect']` to interpret Rounded Rectangles
- Added ability for `Two.RoundedRectangle.radius` to be a `Two.Vector` for x, y component styling
- Added ES6 compatible `./build/two.module.js` for importing library
- Improved `Q` SVG interpretation
- `Two.Texture`, `Two.Sprite`, and `Two.ImageSequence` implemented in `WebGLRenderer`
- Added `className` property to `Two.Shape`s for easier CSS styling in `SVGRenderer` [@fr0](https://github.com/fr0)
- `Two.Events.resize` is now bound to a renderer's `setSize` function giving a more generic solution to change scenegraph items based on dimensions changing
## Dec 1, 2017 v0.7.0-alpha.1
Dec 1, 2017
- Fixed closed `Two.Path.getPointAt` method to clamp properly
- Added `Two.Texture.repeat` for describing pattern invocations
- Added `Two.Texture`, `Two.Sprite`, and `Two.ImageSequence`
- Removed `Two.Shape` inheritance for `Two.Gradient`s
- Added `Two.Vector.rotate` method [@ferm10n](https://github.com/ferm10n)
- Objects clone to parent only if parent exists [@ferm10n](https://github.com/ferm10n)
- Vendor agnostic `requestAnimationFrame` [@ferm10n](https://github.com/ferm10n)
- `Two.Utils.Events.listenTo` and `Two.Utils.Events.stopListening` [@ferm10n](https://github.com/ferm10n)
- `Two.Utils.Events` added to `Two.Path.prototype` for event inheritance [@ferm10n](https://github.com/ferm10n)
- Enhanced `Two.Shape.scale` to allow both numbers and `Two.Vector` as property value
- Made `Two.interpret` use latest primitives
- Added `Two.Circle` primitive
- `Two.Shape.translation` is now a getter/setter and can be replaced
- Fixed translation interpretation to strip out `'px'` strings
- Removed `Two.SineRing` — make `Two.Star.curved = true` and it's the same effect
- Enhanced `Two.ArcSegment`, `Two.Ellipse`, `Two.Polygon`, `Two.Rectangle`, `Two.RoundedRectangle`, `Two.Star`
- Fixed `Two.Anchor.relative` interpretation in `svg`, `canvas`, and `webgl` renderers
- Made Getters / Setters enumerable for iteration compatibility
- Fixed Two.Utils.Collection.splice method and added additional test
- Added compatibility with [node.js](http://nodejs.org/), [browserify](http://browserify.org), and [node-canvas](https://github.com/Automattic/node-canvas)
- Removed third party dependencies
- Added `remove` method to `Two.Text`
- Fixed ordering on same parent additions for `Two.Group`
## Feb 9, 2016 v0.6.0
Feb 9, 2016
- Updated `Two.CanvasRenderer.ctx.imageSmoothingEnabled` to not use deprecated invocation, [issue 178](https://github.com/jonobr1/two.js/issues/178)
- Fixed `Two.Group.mask` in `SVGRenderer` to append to DOM correctly
- Updated `require` imports to be compatible with [require.js](http://requirejs.org/)
- Added `Two.Text` for programmatically writing text in Two.js
## Oct 1, 2015 v0.5.0
Oct 1, 2015
- Added support for `two.interpret` to import `svg`'s gradients
- Added `Two.Utils.xhr` and `two.load` methods to asynchronously load SVG files
- Added `Two.Gradient`, `Two.LinearGradient`, and `Two.RadialGradient`
- Added dependency check to ensure ASM loading in environments like NPM as well as in the browser
- Properly deleted `webgl` textures on removal of `Two.Path`
- Added support for `two.interpret` to import `svg`'s [Elliptical Arcs](http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands)
- Added `Two.ArcSegment` and `Two.SineRing` as new shapes invoked like `Two.Path` [@chrisdelbuck](http://github.com/chrisdelbuck)
- Added `Two.Line`, `Two.Rectangle`, `Two.RoundedRectangle`, `Two.Ellipse`, `Two.Polygon`, and `Two.Star` as new shapes invoked like `Two.Path`
- **_Breaking_**: renamed `Two.Polygon` to `Two.Path`
- Performance enhancements to `webgl` renderer
- Performance enhancements to `canvas` renderer [Leo Koppelkamm](https://github.com/ponychicken)
- Enabled render ordering in `Two.Group.children` based on previous augmentation
- Augmented `Two.Group.children` to inherit from `Two.Collection` effectively making it an array instead of a map [Leo Koppelkamm](https://github.com/ponychicken)
- The map can still be accessed at `Two.Group.children.ids`
## Jul 22, 2014 v0.4.0
Jul 22, 2014
- Updated `Two.interpret` to handle polybezier path data
- Added `Two.Group.mask` and `Two.Polygon.clip` in order to create clipping masks
- `Two.Group` has own `opacity` property [Leo Koppelkamm](https://github.com/ponychicken)
- Rendering optimizations [Leo Koppelkamm](https://github.com/ponychicken)
- `Two.noConflict` non-destructive command internally to the library
- `Two.interpret` decomposes `transform` attribute of source tag
- `Two.interpret` handles item irregularities from [Inkscape](http://www.inkscape.org/)
- Changed `Two.Identifier` to use underscores instead of hyphens for dot-notation access [Leo Koppelkamm](https://github.com/ponychicken)
- Added `Two.Group.getById` and `Two.Group.getByClassName` methods for convenient selection [Leo Koppelkamm](https://github.com/ponychicken)
- Added `classList` to all `Two.Shape`s [Leo Koppelkamm](https://github.com/ponychicken)
- Enabled inference of applied styles on imported svgs [Leo Koppelkamm](https://github.com/ponychicken)
- Added `Two.Polygon.getPointAt` method to get coordinates on a curve/line
- Added `Two.Polygon.length` property and `Two.Polygon._updateLength` method to calculate length of curve/line
- Updated `Two.Group.prototype` observable properties on `Two.Polygon.Properties` to ensure each property is considered unique
- ~~`Two.Polygon.vertices` first and last vertex create automated control points when `Two.Polygon.curved = true`~~
- Updated `Two.Polygon.subdivide` method to accommodate `Two.makeEllipse`
- Enabled `id` to be properly interpreted from SVG elements [@chrisdelbuck](http://github.com/chrisdelbuck)
- Updated `webgl` renderer `getBoundingClientRect` to accommodate `relative` anchors
- Updated `beginning` and `ending` to clamp to each other
- Reorganized `Two.Polygon._update` and `Two.Polygon.plot` in order to handle `beginning` and `ending` properties
- Updated `Two.getComputedMatrix` and `Two.Polygon.getBoundingClientRect` to adhere to nested transformations
- Updated `Two.Anchor` to change `control` points relatively by default through `anchor.relative` property
- Updated `Two.Polygon.subdivide` method to accommodate `curved = false` circumstances
- Updated `svg`, `canvas`, and `webgl` renderers to properly reflect holes in curved `Two.Polygon`s
- Updated `Two.Group` `clone` method
- Added `toObject` method to `Two.Group`, `Two.Polygon`, `Two.Anchor`
- `Two.Polygon` initializes `polygon.cap = 'butt'` and `polygon.join = 'miter'` based on Adobe Illustrator defaults
- `Two.Polygon.subdivide` method now works with `Two.Commands.move` for noncontiguous polygons
- Internally update matrices on `getBoundingClientRect` in order to remove the need to defer or wait for internal variables to be up-to-date
- Refactor of renderers and scenegraph for performance optimization and lower memory footprint
- Relinquished internal _events_ for _flags_
- Prototypical declaration of `Object.defineProperty`
- Added `_update` and `flagReset` methods to `Two.Shape`, `Two.Group`, and `Two.Polygon`
- Decoupled `canvas` and `webgl` renderers and are now independent
- Added `_matrix.manual` to override the default behavior of a `Two.Polygon` transformation
- Localized variables per file as much as possible to reduce Garbage Collection on runtime
## Oct 25, 2013 v0.3.0
Oct 25, 2013
- Can properly pass `domElement` on construction of new instance of two
- Added `overdraw` boolean to `webgl` renderer [@arodic](https://github.com/arodic)
- Added support for ie9 svg interpretation [@tomconroy](https://github.com/tomconroy)
- Added `subdivide` method for `Two.Polygon` and `Two.Group`
- Ensure sure that `manual` properly is set on construction of `Two.Polygon` that it binds `Two.Anchor.controls` change events
- Added automatic High DPI support for `webgl` renderer
- Updated `two.interpret(svg)` to handle compound paths
- Added [`Two.Anchor`](http://jonobr1.github.io/two.js/#two-anchor) which represents all anchor points drawn in two.js
- Modified source to not have any instances of `window` for node use
- Updated to underscore.js 1.5.1
- Added `Two.Utils.getReflection` method to properly get reflection's in svg interpretation
- Made `Two.Vector` inherently not broadcast events and now needs to be explicitly bound to in order to broadcast events, which two.js does internally for you
- Created `Two.Utils.Collection` an observable array-like class that `polygon.vertices` inherit [@fchasen](http://github.com/fchasen)
- Added `Two.Events.insert` and `Two.Events.remove` for use with `Two.Utils.Collection`
- Properly recurses `getBoundingClientRect` for both `Two.Group` and `Two.Polygon`
- Added `Two.Version` to clarify forthcoming builds
- Updated hierarchy ordering of `group.children` in `canvas` and `webgl` renderers
- Updated shallow and bidirectional `remove` method for `Two.Group` and `Two.Polygon`
- Added `corner` method to `Two.Group` and `Two.Polygon` allowing anchoring along the upper lefthand corner of the form
- Modified `center` method of `Two.Group` and `Two.Polygon` to not affect the `translation` property to stay inline with `corner` method and any future orientation and anchoring logic
- Added automatic High DPI support for `canvas` renderer
- Added `overdraw` boolean to `canvas` renderer
- Added AMD loader compatibility [@thomasrudin](http://github.com/thomasrudin)
- Deferred `two.update();` to account for canvas and webgl
- Added `remove` and `clear` methods to `two` instance
- Updated svg interpretation for `webgl` context
- ~~Added matrix property to all `Two.Shape`'s for advanced transformations~~
- Added `inverse` method to `Two.Matrix`
- Remove execution path dependency on utils/build.js [@masonblier](https://github.com/masonblier)
- Added `timeDelta` property to every `two` instance
- Added gruntfile, package.json for more integration with `npm`, and dependency free build (`build/two.clean.js`) [@iros](https://github.com/iros)
- Crossbrowser compatibility with `noStroke` and `noFill` commands
## May 3, 2013 v0.2.0
May 3, 2013
- First alpha release
## Jan 29, 2013 v0.1.0-alpha
Jan 29, 2013
- Proof of Concept built from Three.js
================================================
FILE: wiki/docs/README.md
================================================
================================================
FILE: wiki/docs/anchor/README.md
================================================
---
title: Two.Anchor
pageClass: docs
lang: en-US
---
# Two.Anchor
Extends: [Two.Vector](/docs/vector/)
An object that holds 3 [Two.Vector](/docs/vector/)s, the anchor point and its corresponding handles: `left` and `right`. In order to properly describe the bezier curve about the point there is also a command property to describe what type of drawing should occur when Two.js renders the anchors.
### Constructor
| Argument | Description |
| ---- | ----------- |
| x | The x position of the root anchor point. |
| y | The y position of the root anchor point. |
| ax | The x position of the left handle point. |
| ay | The y position of the left handle point. |
| bx | The x position of the right handle point. |
| by | The y position of the right handle point. |
| command | The command to describe how to render. Applicable commands are [Two.Commands](/docs/two/#commands) |
__Returns__: String
- A String with comma-separated values reflecting the various values on the current instance.
Create a string form of the current instance. Intended for use with storing values in a database. This is lighter to store than the JSON compatible [Two.Anchor.toObject](/docs/anchor/#toobject).
An `Array` like object with additional event propagation on actions. `pop`, `shift`, and `splice` trigger `removed` events. `push`, `unshift`, and `splice` with more than 2 arguments trigger 'inserted'. Finally, `sort` and `reverse` trigger `order` events.
This is the base class for constructing different types of gradients with Two.js. The two common gradients are [Two.LinearGradient](/docs/effects/linear-gradient/) and [Two.RadialGradient](/docs/effects/radial-gradient/).
### Constructor
| Argument | Description |
| ---- | ----------- |
| stops | A list of [Two.Stop](/docs/effects/stop/)s that contain the gradient fill pattern for the gradient. |
Indicates what happens if the gradient starts or ends inside the bounds of the target rectangle. Possible values are `'pad'`, `'reflect'`, and `'repeat'`.
See: [https://www.w3.org/TR/SVG11/pservers.html#LinearGradientElementSpreadMethodAttribute](https://www.w3.org/TR/SVG11/pservers.html#LinearGradientElementSpreadMethodAttribute) for more information
Indicates how coordinate values are interpreted by the renderer. Possible values are `'userSpaceOnUse'` and `'objectBoundingBox'`.
See: [https://www.w3.org/TR/SVG11/pservers.html#RadialGradientElementGradientUnitsAttribute](https://www.w3.org/TR/SVG11/pservers.html#RadialGradientElementGradientUnitsAttribute) for more information
A convenient package to display images scaled to fit specific dimensions. Unlike [Two.Sprite](/docs/effects/sprite/), this class scales the image to the provided width and height rather than using the image's native dimensions. By default, images are scaled to 'fill' within the bounds while preserving aspect ratio.
### Constructor
| Argument | Description |
| ---- | ----------- |
| src | The URL path or [Two.Texture](/docs/effects/texture/) to be used as the bitmap data displayed on the image. |
| ox | The initial `x` position of the Two.Image. |
| oy | The initial `y` position of the Two.Image. |
| width | The width to display the image at. |
| height | The height to display the image at. |
| mode | The fill mode |
Different mode types to render an image inspired by Figma.
__Modes.fill__: Scale image to fill the bounds while preserving aspect ratio.
__Modes.fit__: Scale image to fit within bounds while preserving aspect ratio.
__Modes.crop__: Scale image to fill bounds while preserving aspect ratio, cropping excess.
__Modes.tile__: Repeat image at original size to fill the bounds.
__Modes.stretch__: Stretch image to fill dimensions, ignoring aspect ratio.
Release the image's renderer resources and detach all events.
This method disposes the texture (calling dispose() for thorough cleanup) and inherits comprehensive
cleanup from the Rectangle/Path hierarchy while preserving the renderer type
for potential re-attachment.
A convenient package to display still or animated images organized as a series of still images.
### Constructor
| Argument | Description |
| ---- | ----------- |
| src | A list of URLs or [Two.Texture](/docs/effects/texture/)s. |
| ox | The initial `x` position of the Two.ImageSequence. |
| oy | The initial `y` position of the Two.ImageSequence. |
| frameRate | The frame rate at which the images should playback at. |
| Argument | Description |
| ---- | ----------- |
| obj | Object notation of a [Two.ImageSequence](/docs/effects/image-sequence/) to create a new instance |
Create a new [Two.ImageSequence](/docs/effects/image-sequence/) from an object notation of a [Two.ImageSequence](/docs/effects/image-sequence/).
| Argument | Description |
| ---- | ----------- |
| firstFrame | The index of the frame to start the animation with. |
| lastFrame | The index of the frame to end the animation with. Defaults to the last item in the [Two.ImageSequence.textures](/docs/effects/image-sequence/#textures). |
| onLastFrame | Optional callback function to be triggered after playing the last frame. This fires multiple times when the image sequence is looped. |
Initiate animation playback of a [Two.ImageSequence](/docs/effects/image-sequence/).
Release the image sequence's renderer resources and detach all events.
This method stops any running animation, clears animation callbacks, unbinds
textures collection events, and disposes individual textures (calling dispose()
for thorough cleanup) while preserving the renderer type for potential
re-attachment to a new renderer.
### Constructor
| Argument | Description |
| ---- | ----------- |
| x1 | The x position of the first end point of the linear gradient. |
| y1 | The y position of the first end point of the linear gradient. |
| x2 | The x position of the second end point of the linear gradient. |
| y2 | The y position of the second end point of the linear gradient. |
| stops | A list of [Two.Stop](/docs/effects/stop/)s that contain the gradient fill pattern for the gradient. |
| Argument | Description |
| ---- | ----------- |
| obj | Object notation of a [Two.LinearGradient](/docs/effects/linear-gradient/) to create a new instance |
Create a new [Two.LinearGradient](/docs/effects/linear-gradient/) from an object notation of a [Two.LinearGradient](/docs/effects/linear-gradient/).
### Constructor
| Argument | Description |
| ---- | ----------- |
| x | The x position of the origin of the radial gradient. |
| y | The y position of the origin of the radial gradient. |
| radius | The radius of the radial gradient. |
| stops | A list of [Two.Stop](/docs/effects/stop/)s that contain the gradient fill pattern for the gradient. |
| focalX | The x position of the focal point on the radial gradient. |
| focalY | The y position of the focal point on the radial gradient. |
| Argument | Description |
| ---- | ----------- |
| obj | Object notation of a [Two.RadialGradient](/docs/effects/radial-gradient/) to create a new instance |
Create a new [Two.RadialGradient](/docs/effects/radial-gradient/) from an object notation of a [Two.RadialGradient](/docs/effects/radial-gradient/).
A convenient package to display still or animated images through a tiled image source. For more information on the principals of animated imagery through tiling see [Texture Atlas](https://en.wikipedia.org/wiki/Texture_atlas) on Wikipedia.
### Constructor
| Argument | Description |
| ---- | ----------- |
| src | The URL path or [Two.Texture](/docs/effects/texture/) to be used as the bitmap data displayed on the sprite. |
| ox | The initial `x` position of the Two.Sprite. |
| oy | The initial `y` position of the Two.Sprite. |
| cols | The number of columns the sprite contains. |
| rows | The number of rows the sprite contains. |
| frameRate | The frame rate at which the partitions of the image should playback at. |
| Argument | Description |
| ---- | ----------- |
| firstFrame | The index of the frame to start the animation with. |
| lastFrame | The index of the frame to end the animation with. Defaults to the last item in the [Two.Sprite.textures](/docs/effects/sprite/#textures). |
| onLastFrame | Optional callback function to be triggered after playing the last frame. This fires multiple times when the sprite is looped. |
Initiate animation playback of a [Two.Sprite](/docs/effects/sprite/).
Release the sprite's renderer resources and detach all events.
This method stops any running animation, clears animation callbacks, disposes
the texture (calling dispose() for thorough cleanup), and inherits comprehensive
cleanup from the Rectangle/Path hierarchy while preserving the renderer type
for potential re-attachment.
### Constructor
| Argument | Description |
| ---- | ----------- |
| offset | The offset percentage of the stop represented as a zero-to-one value. Default value flip flops from zero-to-one as new stops are created. |
| color | The color of the stop. Default value flip flops from white to black as new stops are created. |
| opacity | The opacity value. Default value is 1, cannot be lower than 0. |
::: tip nota-bene
This is only supported on the [Two.SVGRenderer](/docs/renderers/svg/). You can get the same effect by encoding opacity into `rgba` strings in the color.
:::
Fundamental to work with bitmap data, a.k.a. pregenerated imagery, in Two.js. Supported formats include jpg, png, gif, and tiff. See [Two.Texture.RegularExpressions](/docs/effects/texture/#regularexpressions) for a full list of supported formats.
### Constructor
| Argument | Description |
| ---- | ----------- |
| src | The URL path to an image file or an `` element. |
| callback | An optional callback function once the image has been loaded. |
Loads an image as a buffer in headless environments.
| Argument | Description |
| ---- | ----------- |
| texture | The [Two.Texture](/docs/effects/texture/) to be loaded. |
| onLoad | The callback function to be triggered once the image is loaded. |
__Returns__: HTMLImageElement
- Returns either a cached version of the image or a new one that is registered in [Two.Texture.ImageRegistry](/docs/effects/texture/#imageregistry).
Convenience function to set [Two.Texture.image](/docs/effects/texture/#image) properties with canonical versions set in [Two.Texture.ImageRegistry](/docs/effects/texture/#imageregistry).
| Argument | Description |
| ---- | ----------- |
| src | The URL path of the image. |
| Argument | Description |
| ---- | ----------- |
| texture | The texture to load. |
| callback | The function to be called once the texture is loaded. |
The corresponding DOM Element of the texture. Can be a ``, ``, or `` element. See [Two.Texture.RegularExpressions](/docs/effects/texture/#regularexpressions) for a full list of supported elements.
::: tip nota-bene
In headless environments this is a `Canvas.Image` object. See [https://github.com/Automattic/node-canvas](https://github.com/Automattic/node-canvas) for more information on headless image objects.
:::
| Argument | Description |
| ---- | ----------- |
| name | The name of the event to bind a function to. |
| handler | The function to be invoked when the event is dispatched. |
| Argument | Description |
| ---- | ----------- |
| name | The name of the event intended to be removed. |
| handler | The handler intended to be removed. |
Call to remove listeners from a specific event. If only `name` is passed then all the handlers attached to that `name` will be removed. If no arguments are passed then all handlers for every event on the obejct are removed.
| Argument | Description |
| ---- | ----------- |
| name | The name of the event to dispatch. |
| args | Anything can be passed after the name and those will be passed on to handlers attached to the event in the order they are passed. |
Call to trigger a custom event. Any additional arguments passed after the name will be passed along to the attached handlers.
### Constructor
| Argument | Description |
| ---- | ----------- |
| x | The x position of the arc. |
| y | The y position of the arc. |
| width | The width, horizontal diameter, of the arc. |
| height | The height, vertical diameter, of the arc. |
| startAngle | The starting angle of the arc in radians. |
| endAngle | The ending angle of the arc in radians. |
| resolution | The number of vertices used to construct the circle. |
================================================
FILE: wiki/docs/extras/zui/README.md
================================================
---
title: Two.ZUI
pageClass: docs
lang: en-US
---
# Two.ZUI
[Two.ZUI](/docs/two/#zui) is an extra class to turn your Two.js scene into a Google Maps or Adobe Illustrator style interface. See [https://codepen.io/jonobr1/pen/PobMKwb](https://codepen.io/jonobr1/pen/PobMKwb) for example usage.
### Constructor
| Argument | Description |
| ---- | ----------- |
| group | The scene or group to |
| domElement | The HTML Element to attach event listeners to. |
| Argument | Description |
| ---- | ----------- |
| min | The minimum scale the ZUI can zoom out to. |
| max | The maximum scale the ZUI can zoom in to. |
__Returns__: Object
- An object with x, y, and z components
| Argument | Description |
| ---- | ----------- |
| a | The x component of position to be transformed. |
| b | The y component of position to be transformed. |
| c | The optional z component of position to be transformed. |
Convert an x, y coordinate in the user’s space to the object's projected space. Optionally pass a z property on the object to apply depth.
__Returns__: Object
- An object with x, y, and z components
| Argument | Description |
| ---- | ----------- |
| a | The x component of position to be transformed. |
| b | The y component of position to be transformed. |
| c | The optional z component of position to be transformed. |
Convert an x, y coordinate in projected space to the user’s space. Optionally pass a z property on the object to apply depth.
| Argument | Description |
| ---- | ----------- |
| byF | The factor to scale by. |
| clientX | The x position of the user's input. |
| clientY | The y position of the user's input. |
A function to zoom by an incremental amount and a position. Typically used for pinch-and-zoom or mousewheel effects.
| Argument | Description |
| ---- | ----------- |
| zoom | The level of the zoom. |
| clientX | The x position of the user's input. |
| clientY | The y position of the user's input. |
A function to set the zoom amount and the origin position. This is used internally by {@Two.ZUI#zoomBy}.
This is the primary class for grouping objects that are then drawn in Two.js. In Illustrator this is a group, in After Effects it would be a Null Object. Whichever the case, the `Two.Group` contains a transformation matrix and commands to style its children, but it by itself doesn't render to the screen.
### Constructor
| Argument | Description |
| ---- | ----------- |
| children | A list of objects that inherit [Two.Shape](/docs/shape/). For instance, the array could be a [Two.Path](/docs/path/), [Two.Text](/docs/text/), and [Two.RoundedRectangle](/docs/shapes/rounded-rectangle/). |
The value of what all child shapes should be filled in with.
See: [https://developer.mozilla.org/en-US/docs/Web/CSS/color_value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) for more information on CSS's colors as `String`.
The value of what all child shapes should be outlined in with.
See: [https://developer.mozilla.org/en-US/docs/Web/CSS/color_value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) for more information on CSS's colors as `String`.
::: tip nota-bene
For [Two.CanvasRenderer](/docs/renderers/canvas/) and [Two.WebGLRenderer](/docs/renderers/webgl/) when set to false all updating is disabled improving performance dramatically with many objects in the scene.
:::
Determines whether or not Two.js should calculate curves, lines, and commands automatically for you or to let the developer manipulate them for themselves.
Number between zero and one to state the beginning of where the path is rendered.
[Two.Group.beginning](/docs/group/#beginning) is a percentage value that represents at what percentage into all child shapes should the renderer start drawing.
Number between zero and one to state the ending of where the path is rendered.
[Two.Group.ending](/docs/group/#ending) is a percentage value that represents at what percentage into all child shapes should the renderer start drawing.
Release the group's renderer resources and detach all events.
This method recursively disposes all child objects, unbinds the children
collection events, and preserves the renderer type for potential re-attachment
to a new renderer.
__Returns__: Array.\
Ordered list of intersecting shapes, front to back.
| Argument | Description |
| ---- | ----------- |
| x | X coordinate in world space. |
| y | Y coordinate in world space. |
| options | Hit test configuration. |
| options.visibleOnly | Limit results to visible shapes. |
| options.includeGroups | Include groups in the hit results. |
| options.mode | Whether to return all intersecting shapes or only the top-most. |
| options.deepest | Alias for `mode: 'deepest'`. |
| options.precision | Segmentation precision for curved geometry. |
| options.tolerance | Pixel tolerance applied to hit testing. |
| options.fill | Override fill testing behaviour. |
| options.stroke | Override stroke testing behaviour. |
| options.filter | Predicate to filter shapes from the result set. |
Traverse the group hierarchy and return shapes that contain the specified point.
::: tip nota-bene
Expects *world-space coordinates* – the same pixel-space you get from the renderer (e.g., mouse `clientX`/`clientY` adjusted for the canvas’s offset and pixel ratio).
:::
__Returns__: Array.\
- Empty array if nothing is found.
Recursively search for children of a specific type, e.g. [Two.Path](/docs/path/). Pass a reference to this type as the param. Returns an array of matching elements.
| Argument | Description |
| ---- | ----------- |
| objects | An array of objects to be removed. Can be also removed as individual arguments. If no arguments are passed, then it removes itself from its parent. |
When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space for all child shapes.
When `strokeAttenuation` is `false`, this property is applied to all child shapes, making their stroke widths automatically adjust to compensate for the group's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke widths scale normally with transformations.
================================================
FILE: wiki/docs/matrix/README.md
================================================
---
title: Two.Matrix
pageClass: docs
lang: en-US
---
# Two.Matrix
A class to store 3 x 3 transformation matrix information. In addition to storing data `Two.Matrix` has suped up methods for commonplace mathematical operations.
### Constructor
| Argument | Description |
| ---- | ----------- |
| a | The value for element at the first column and first row. |
| b | The value for element at the second column and first row. |
| c | The value for element at the third column and first row. |
| d | The value for element at the first column and second row. |
| e | The value for element at the second column and second row. |
| f | The value for element at the third column and second row. |
| g | The value for element at the first column and third row. |
| h | The value for element at the second column and third row. |
| i | The value for element at the third column and third row. |
__Returns__: Array.\
- If an optional `C` matrix isn't passed then a new one is created and returned.
| Argument | Description |
| ---- | ----------- |
| A | The first [Two.Matrix](/docs/matrix/) to multiply |
| B | The second [Two.Matrix](/docs/matrix/) to multiply |
| C | An optional [Two.Matrix](/docs/matrix/) to apply the result to |
Multiply two matrices together and return the result.
::: tip nota-bene
- Setting to `true` nullifies [Two.Shape.translation](/docs/shape/#translation), [Two.Shape.rotation](/docs/shape/#rotation), and [Two.Shape.scale](/docs/shape/#scale).
:::
| Argument | Description |
| ---- | ----------- |
| a | The value for element at the first column and first row |
| b | The value for element at the second column and first row |
| c | The value for element at the third column and first row |
| d | The value for element at the first column and second row |
| e | The value for element at the second column and second row |
| f | The value for element at the third column and second row |
| g | The value for element at the first column and third row |
| h | The value for element at the second column and third row |
| i | The value for element at the third column and third row |
Set an array of values onto the matrix. Order described in [Two.Matrix](/docs/matrix/).
| Argument | Description |
| ---- | ----------- |
| x | The `x` component to be multiplied. |
| y | The `y` component to be multiplied. |
| z | The `z` component to be multiplied. |
Multiply all components of a matrix against a 3 component vector.
| Argument | Description |
| ---- | ----------- |
| a | The value at the first column and first row of the matrix to be multiplied. |
| b | The value at the second column and first row of the matrix to be multiplied. |
| c | The value at the third column and first row of the matrix to be multiplied. |
| d | The value at the first column and second row of the matrix to be multiplied. |
| e | The value at the second column and second row of the matrix to be multiplied. |
| f | The value at the third column and second row of the matrix to be multiplied. |
| g | The value at the first column and third row of the matrix to be multiplied. |
| h | The value at the second column and third row of the matrix to be multiplied. |
| i | The value at the third column and third row of the matrix to be multiplied. |
Multiply all components of a matrix against another matrix.
| Argument | Description |
| ---- | ----------- |
| fullMatrix | Return the full 9 elements of the matrix or just 6 in the format for 2D transformations. |
| output | An array empty or otherwise to apply the values to. |
Create a transform array. Used for the Two.js rendering APIs.
| Argument | Description |
| ---- | ----------- |
| fullMatrix | Return the full 9 elements of the matrix or just 6 for 2D transformations. |
| output | An array empty or otherwise to apply the values to. |
Create a transform array. Used for the Two.js rendering APIs.
This is the primary primitive class for creating all drawable shapes in Two.js. Unless specified methods return their instance of `Two.Path` for the purpose of chaining.
### Constructor
| Argument | Description |
| ---- | ----------- |
| vertices | A list of [Two.Anchor](/docs/anchor/)s that represent the order and coordinates to construct the rendered shape. |
| closed | Describes whether the shape is closed or open. |
| curved | Describes whether the shape automatically calculates bezier handles for each vertex. |
| manual | Describes whether the developer controls how vertices are plotted or if Two.js automatically plots coordinates based on closed and curved booleans. |
The value of what the path should be filled in with.
See: [https://developer.mozilla.org/en-US/docs/Web/CSS/color_value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) for more information on CSS's colors as `String`.
The value of what the path should be outlined in with.
See: [https://developer.mozilla.org/en-US/docs/Web/CSS/color_value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) for more information on CSS's colors as `String`.
::: tip nota-bene
For [Two.CanvasRenderer](/docs/renderers/canvas/) and [Two.WebGLRenderer](/docs/renderers/webgl/) when set to false all updating is disabled improving performance dramatically with many objects in the scene.
:::
Determines whether or not Two.js should calculate curves, lines, and commands automatically for you or to let the developer manipulate them for themselves.
Array of numbers. Odd indices represent dash length. Even indices represent dash space.
A list of numbers that represent the repeated dash length and dash space applied to the stroke of the text.
See: [https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray) for more information on the SVG stroke-dasharray attribute.
Release the path's renderer resources and detach all events.
This method cleans up vertices collection events, individual vertex events,
control point events, and disposes fill/stroke effects (calling dispose()
on Gradients and Textures for thorough cleanup) while preserving the
renderer type for potential re-attachment to a new renderer.
| Argument | Description |
| ---- | ----------- |
| x | x coordinate to hit test against |
| y | y coordinate to hit test against |
| options | Optional options object |
| options.ignoreVisibility | If `true`, hit test against `path.visible = false` shapes |
| options.tolerance | Padding to hit test against in pixels |
Check to see if coordinates are within a [Two.Path](/docs/path/)'s bounding rectangle
::: tip nota-bene
Expects *world-space coordinates* – the same pixel-space you get from the renderer (e.g., mouse `clientX`/`clientY` adjusted for the canvas’s offset and pixel ratio).
:::
| Argument | Description |
| ---- | ----------- |
| t | Percentage value describing where on the [Two.Path](/docs/path/) to estimate and assign coordinate values. |
| obj | Object to apply calculated x, y to. If none available returns new `Object`. |
Given a float `t` from 0 to 1, return a point or assign a passed `obj`'s coordinates to that percentage on this [Two.Path](/docs/path/)'s curve.
| Argument | Description |
| ---- | ----------- |
| options | Configuration for smoothing. |
| options.type | Type of smoothing algorithm. |
| options.from | Index of vertices to start smoothing |
| options.to | Index of vertices to terminate smoothing |
Adjust vertex handles to generate smooth curves without toggling `automatic`.
::: tip nota-bene
This property is currently not working because of SVG spec issues found here {@link https://code.google.com/p/chromium/issues/detail?id=370951}.
:::
When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
================================================
FILE: wiki/docs/registry/README.md
================================================
---
title: Two.Registry
pageClass: docs
lang: en-US
---
# Two.Registry
An arbitrary class to manage a directory of things. Mainly used for keeping tabs of textures in Two.js.
This class is used by [Two]() when constructing with `type` of `Two.Types.canvas`. It takes Two.js' scenegraph and renders it to a ``.
### Constructor
| Argument | Description |
| ---- | ----------- |
| parameters | This object is inherited when constructing a new instance of [Two](). |
| parameters.domElement | The `` to draw to. If none given a new one will be constructed. |
| parameters.overdraw | Determines whether the canvas should clear the background or not. Defaults to `true`. |
| parameters.smoothing | Determines whether the canvas should antialias drawing. Set it to `false` when working with pixel art. `false` can lead to better performance, since it would use a cheaper interpolation algorithm. |
| Argument | Description |
| ---- | ----------- |
| width | The new width of the renderer. |
| height | The new height of the renderer. |
| ratio | The new pixel ratio (pixel density) of the renderer. Defaults to calculate the pixel density of the user's screen. |
This class is used by [Two]() when constructing with `type` of `Two.Types.svg` (the default type). It takes Two.js' scenegraph and renders it to a ``.
### Constructor
| Argument | Description |
| ---- | ----------- |
| parameters | This object is inherited when constructing a new instance of [Two](). |
| parameters.domElement | The `` to draw to. If none given a new one will be constructed. |
This class is used by [Two]() when constructing with `type` of `Two.Types.webgl`. It takes Two.js' scenegraph and renders it to a `` through the WebGL api.
### Constructor
| Argument | Description |
| ---- | ----------- |
| parameters | This object is inherited when constructing a new instance of [Two](). |
| parameters.domElement | The `` to draw to. If none given a new one will be constructed. |
| parameters.offscreenElement | The offscreen two dimensional `` to render each element on WebGL texture updates. |
| parameters.antialias | Determines whether the canvas should clear render with antialias on. |
| Argument | Description |
| ---- | ----------- |
| width | The new width of the renderer. |
| height | The new height of the renderer. |
| ratio | The new pixel ratio (pixel density) of the renderer. Defaults to calculate the pixel density of the user's screen. |
::: tip nota-bene
[Two.Shape.position](/docs/shape/#position), [Two.Shape.rotation](/docs/shape/#rotation), [Two.Shape.scale](/docs/shape/#scale), [Two.Shape.skewX](/docs/shape/#skewx), and [Two.Shape.skewY](/docs/shape/#skewy) apply their values to the matrix when changed. The matrix is what is sent to the renderer to be drawn.
:::
::: tip nota-bene
This value can be replaced with a [Two.Vector](/docs/vector/) to do non-uniform scaling. e.g: `shape.scale = new Two.Vector(2, 1);`
:::
| Argument | Description |
| ---- | ----------- |
| x | x coordinate to hit test against |
| y | y coordinate to hit test against |
| options | Optional options object |
| options.ignoreVisibility | If `true`, hit test against `shape.visible = false` shapes |
| options.tolerance | Padding to hit test against in pixels |
Check to see if coordinates are within a [Two.Shape](/docs/shape/)'s bounding rectangle
::: tip nota-bene
Expects *world-space coordinates* – the same pixel-space you get from the renderer (e.g., mouse `clientX`/`clientY` adjusted for the canvas’s offset and pixel ratio).
:::
### Constructor
| Argument | Description |
| ---- | ----------- |
| x | The x position of the arc segment. |
| y | The y position of the arc segment. |
| innerRadius | The inner radius value of the arc segment. |
| outerRadius | The outer radius value of the arc segment. |
| startAngle | The start angle of the arc segment in Number. |
| endAngle | The end angle of the arc segment in Number. |
| resolution | The number of vertices used to construct the arc segment. |
### Constructor
| Argument | Description |
| ---- | ----------- |
| x | The x position of the circle. |
| y | The y position of the circle. |
| radius | The radius value of the circle. |
| resolution | The number of vertices used to construct the circle. |
### Constructor
| Argument | Description |
| ---- | ----------- |
| x | The x position of the ellipse. |
| y | The y position of the ellipse. |
| rx | The radius value of the ellipse in the x direction. |
| ry | The radius value of the ellipse in the y direction. |
| resolution | The number of vertices used to construct the ellipse. |
### Constructor
| Argument | Description |
| ---- | ----------- |
| x1 | The x position of the first vertex on the line. |
| y1 | The y position of the first vertex on the line. |
| x2 | The x position of the second vertex on the line. |
| y2 | The y position of the second vertex on the line. |
================================================
FILE: wiki/docs/shapes/points/README.md
================================================
---
title: Two.Points
pageClass: docs
lang: en-US
---
# Two.Points
Extends: [Two.Shape](/docs/shape/)
This is a primary primitive class for quickly and easily drawing points in Two.js. Unless specified methods return their instance of `Two.Points` for the purpose of chaining.
### Constructor
| Argument | Description |
| ---- | ----------- |
| vertices | A list of [Two.Vector](/docs/vector/)s that represent the order and coordinates to construct a rendered set of points. |
Number between zero and one to state the beginning of where the path is rendered.
[Two.Points.beginning](/docs/shapes/points/#beginning) is a percentage value that represents at what percentage into the path should the renderer start drawing.
Number between zero and one to state the ending of where the path is rendered.
[Two.Points.ending](/docs/shapes/points/#ending) is a percentage value that represents at what percentage into the path should the renderer start drawing.
The value of what the path should be filled in with.
See: [https://developer.mozilla.org/en-US/docs/Web/CSS/color_value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) for more information on CSS's colors as `String`.
The value of what the path should be outlined in with.
See: [https://developer.mozilla.org/en-US/docs/Web/CSS/color_value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) for more information on CSS's colors as `String`.
::: tip nota-bene
For [Two.CanvasRenderer](/docs/renderers/canvas/) and [Two.WebGLRenderer](/docs/renderers/webgl/) when set to false all updating is disabled improving performance dramatically with many objects in the scene.
:::
Array of numbers. Odd indices represent dash length. Even indices represent dash space.
A list of numbers that represent the repeated dash length and dash space applied to the stroke of the text.
See: [https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray) for more information on the SVG stroke-dasharray attribute.
Release the points' renderer resources and detach all events.
This method cleans up vertices collection events, individual vertex events,
and disposes fill/stroke effects (calling dispose() on Gradients and
Textures for thorough cleanup) while preserving the renderer type for
potential re-attachment to a new renderer.
When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
### Constructor
| Argument | Description |
| ---- | ----------- |
| x | The x position of the polygon. |
| y | The y position of the polygon. |
| radius | The radius value of the polygon. |
| sides | The number of vertices used to construct the polygon. |
::: tip nota-bene
This property is tied to [Two.Polygon.width](/docs/shapes/polygon/#width) and [Two.Polygon.height](/docs/shapes/polygon/#height). When you set `radius`, it affects `width` and `height`. Likewise, if you set `width` or `height` it will change the `radius`.
:::
::: tip nota-bene
This property is tied to [Two.Polygon.radius](/docs/shapes/polygon/#radius). When you set `radius`, it affects the `width`. Likewise, if you set `width` it will change the `radius`.
:::
::: tip nota-bene
This property is tied to [Two.Polygon.radius](/docs/shapes/polygon/#radius). When you set `radius`, it affects the `height`. Likewise, if you set `height` it will change the `radius`.
:::
### Constructor
| Argument | Description |
| ---- | ----------- |
| x | The x position of the rectangle. |
| y | The y position of the rectangle. |
| width | The width value of the rectangle. |
| height | The width value of the rectangle. |
### Constructor
| Argument | Description |
| ---- | ----------- |
| x | The x position of the rounded rectangle. |
| y | The y position of the rounded rectangle. |
| width | The width value of the rounded rectangle. |
| height | The width value of the rounded rectangle. |
| radius | The radius value of the rounded rectangle. |
| resolution | The number of vertices used to construct the rounded rectangle. |
| Argument | Description |
| ---- | ----------- |
| obj | Object notation of a [Two.RoundedRectangle](/docs/shapes/rounded-rectangle/) to create a new instance |
Create a new [Two.RoundedRectangle](/docs/shapes/rounded-rectangle/) from an object notation of a [Two.RoundedRectangle](/docs/shapes/rounded-rectangle/).
### Constructor
| Argument | Description |
| ---- | ----------- |
| x | The x position of the star. |
| y | The y position of the star. |
| innerRadius | The inner radius value of the star. |
| outerRadius | The outer radius value of the star. |
| sides | The number of sides used to construct the star. |
This is a primitive class for creating drawable text that can be added to the scenegraph.
### Constructor
| Argument | Description |
| ---- | ----------- |
| message | The String to be rendered to the scene. |
| x | The position in the x direction for the object. |
| y | The position in the y direction for the object. |
| styles | An object where styles are applied. Attribute must exist in Two.Text.Properties. |
The font family Two.js should attempt to register for rendering. The default value is `'sans-serif'`. Comma separated font names can be supplied as a "stack", similar to the CSS implementation of `font-family`.
Alignment of text in relation to [Two.Text.translation](/docs/text/#translation)'s coordinates. Possible values include `'left'`, `'center'`, `'right'`. Defaults to `'center'`.
The vertical aligment of the text in relation to [Two.Text.translation](/docs/text/#translation)'s coordinates. Possible values include `'top'`, `'middle'`, `'bottom'`, and `'baseline'`. Defaults to `'baseline'`.
::: tip nota-bene
In headless environments where the canvas is based on [https://github.com/Automattic/node-canvas](https://github.com/Automattic/node-canvas), `baseline` seems to be the only valid property.
:::
A number at intervals of 100 to describe the font's weight. This compatibility varies with the typeface's variant weights. Larger values are bolder. Smaller values are thinner. Defaults to `'500'`.
String to determine what direction the text should run. Possibly values are `'ltr'` for left-to-right and `'rtl'` for right-to-left. Defaults to `'ltr'`.
The value of what the text object should be filled in with.
See: [https://developer.mozilla.org/en-US/docs/Web/CSS/color_value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) for more information on CSS's colors as `String`.
The value of what the text object should be filled in with.
See: [https://developer.mozilla.org/en-US/docs/Web/CSS/color_value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) for more information on CSS's colors as `String`.
::: tip nota-bene
For [Two.CanvasRenderer](/docs/renderers/canvas/) and [Two.WebGLRenderer](/docs/renderers/webgl/) when set to false all updating is disabled improving performance dramatically with many objects in the scene.
:::
::: tip nota-bene
This property is currently not working because of SVG spec issues found here {@link https://code.google.com/p/chromium/issues/detail?id=370951}.
:::
::: tip nota-bene
This property is currently not working because of SVG spec issues found here {@link https://code.google.com/p/chromium/issues/detail?id=370951}.
:::
Array of numbers. Odd indices represent dash length. Even indices represent dash space.
A list of numbers that represent the repeated dash length and dash space applied to the stroke of the text.
See: [https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray) for more information on the SVG stroke-dasharray attribute.
Release the text's renderer resources and detach all events.
This method disposes fill and stroke effects (calling dispose() on
Gradients and Textures for thorough cleanup) while preserving the
renderer type for potential re-attachment to a new renderer.
When set to `true`, stroke width scales with transformations (default behavior). When `false`, stroke width remains constant in screen space.
When `strokeAttenuation` is `false`, the stroke width is automatically adjusted to compensate for the object's world transform scale, maintaining constant visual thickness regardless of zoom level. When `true` (default), stroke width scales normally with transformations.
================================================
FILE: wiki/docs/two/README.md
================================================
---
title: Two
pageClass: docs
lang: en-US
---
# Two
Extends: [Two.Events](/docs/events/)
The entrypoint for Two.js. Instantiate a `new Two` in order to setup a scene to render to. `Two` is also the publicly accessible namespace that all other sub-classes, functions, and utilities attach to.
### Constructor
| Argument | Description |
| ---- | ----------- |
| options | |
| options.fullscreen | Set to `true` to automatically make the stage adapt to the width and height of the parent document. This parameter overrides `width` and `height` parameters if set to `true`. This overrides `options.fitted` as well. |
| options.fitted | Set to `true` to automatically make the stage adapt to the width and height of the parent element. This parameter overrides `width` and `height` parameters if set to `true`. |
| options.width | The width of the stage on construction. This can be set at a later time. |
| options.height | The height of the stage on construction. This can be set at a later time. |
| options.type | The type of renderer to setup drawing with. See [Two.Types](/docs/two/#types) for available options. |
| options.autostart | Set to `true` to add the instance to draw on `requestAnimationFrame`. This is a convenient substitute for [Two.play](/docs/two/#play). |
| options.domElement | The canvas or SVG element to draw into. This overrides the `options.type` argument. |
When importing SVGs through the [Two.interpret](/docs/two/#interpret) and [Two.load](/docs/two/#load), this boolean determines whether Two.js infers and then overrides the exact transformation matrix of the reference SVG.
A massive object filled with utility functions and properties.
__Two.Utils.read__: A collection of SVG parsing functions indexed by element name.
__Two.Utils.read.path__: Parse SVG path element or `d` attribute string.
The base level [Two.Group](/docs/group/) which houses all objects for the instance. Because it is a [Two.Group](/docs/group/) transformations can be applied to it that will affect all objects in the instance. This is handy as a makeshift inverted camera.
If `options.fullscreen` or `options.fitted` in construction create this function. It sets the `width` and `height` of the instance to its respective parent `window` or `element` depending on the `options` passed.
__Returns__: Two.Element
The object passed for event deallocation.
| Argument | Description |
| ---- | ----------- |
| obj | Object to release from event listening. If none provided then the root [Two.Group](/docs/group/) will be used. |
Release a [Two.Element](/docs/element/)’s events from memory and recurse through its children, effects, and/or vertices.
__Returns__: Array.\
Ordered list of shapes under the specified point, front to back.
| Argument | Description |
| ---- | ----------- |
| x | X coordinate in world space. |
| y | Y coordinate in world space. |
| options | Hit test configuration. |
| options.visibleOnly | Limit results to visible shapes. |
| options.includeGroups | Include groups in the hit results. |
| options.mode | Whether to return all intersecting shapes or only the top-most. |
| options.deepest | Alias for `mode: 'deepest'`. |
| options.precision | Segmentation precision for curved geometry. |
| options.tolerance | Pixel tolerance applied to hit testing. |
| options.fill | Override fill testing behaviour. |
| options.stroke | Override stroke testing behaviour. |
| options.filter | Predicate to filter shapes from the result set. |
Returns shapes underneath the provided coordinates. Coordinates are expected in world space (matching the renderer output).
Removes all objects from the instance's scene. If you intend to have the browser garbage collect this, don't forget to delete the references in your application as well.
__Returns__: Two.Path
- Where `path.curved` is set to `true`.
| Argument | Description |
| ---- | ----------- |
| points | An array of [Two.Anchor](/docs/anchor/) points. |
| | Alternatively you can pass alternating `x` / `y` coordinate values as individual arguments. These will be combined into [Two.Anchor](/docs/anchor/)s for use in the path. |
Creates a Two.js path that is curved and adds it to the scene.
::: tip nota-bene
In either case of passing an array or passing numbered arguments the last argument is an optional `Boolean` that defines whether the path should be open or closed.
:::
| Argument | Description |
| ---- | ----------- |
| points | An array of [Two.Vector](/docs/vector/) points |
| | Alternatively you can pass alternating `x` / `y` coordinate values as individual agrguments. These will be combined into [Two.Vector](/docs/vector/)s for use in the points object. |
Creates a Two.js points object and adds it to the current scene.
| Argument | Description |
| ---- | ----------- |
| points | An array of [Two.Anchor](/docs/anchor/) points |
| | Alternatively you can pass alternating `x` / `y` coordinate values as individual arguments. These will be combined into [Two.Anchor](/docs/anchor/)s for use in the path. |
::: tip nota-bene
In either case of passing an array or passing numbered arguments the last argument is an optional `Boolean` that defines whether the path should be open or closed.
:::
| Argument | Description |
| ---- | ----------- |
| x1 | |
| y1 | |
| x2 | |
| y2 | |
| args | Any number of color stops sometimes referred to as ramp stops. If none are supplied then the default black-to-white two stop gradient is applied. |
Creates a Two.js linear gradient and adds it to the scene. In the case of an effect it's added to an invisible "definitions" group.
| Argument | Description |
| ---- | ----------- |
| x1 | |
| y1 | |
| radius | |
| args | Any number of color stops sometimes referred to as ramp stops. If none are supplied then the default black-to-white two stop gradient is applied. |
Creates a Two.js linear-gradient object and adds it to the scene. In the case of an effect it's added to an invisible "definitions" group.
| Argument | Description |
| ---- | ----------- |
| src | The URL path to an image or a DOM image-like element. |
| callback | Function to be invoked when the image is loaded. |
| Argument | Description |
| ---- | ----------- |
| objects | Two.js objects to be added to the group in the form of an array or as individual arguments. |
Creates a Two.js group object and adds it to the scene.
| Argument | Description |
| ---- | ----------- |
| svg | The SVG node to be parsed. |
| shallow | Don't create a top-most group but append all content directly. |
| add | – Automatically add the reconstructed SVG node to scene. |
Interpret an SVG Node and add it to this instance's scene. The distinction should be made that this doesn't `import` svg's, it solely interprets them into something compatible for Two.js - this is slightly different than a direct transcription.
| Argument | Description |
| ---- | ----------- |
| pathOrSVGContent | The URL path of an SVG file or an SVG document as text. |
| callback | Function to call once loading has completed. |
Load an SVG file or SVG text and interpret it into Two.js legible objects.
A class to store `x` / `y` component vector data. In addition to storing data `Two.Vector` has suped up methods for commonplace mathematical operations.
### Constructor
| Argument | Description |
| ---- | ----------- |
| x | Any number to represent the horizontal `x` component of the vector. |
| y | Any number to represent the vertical `y` component of the vector. |
Get the length of the vector to the power of two. Widely used as less expensive than [Two.Vector.length](/docs/vector/#length) because it isn't square-rooting any numbers.
Get the distance between two vectors to the power of two. Widely used as less expensive than [Two.Vector.distanceTo](/docs/vector/#distanceto) because it isn't square-rooting any numbers.
| Argument | Description |
| ---- | ----------- |
| v | The destination vector to step towards. |
| t | The zero to one value of how close the current vector gets to the destination vector. |
Linear interpolate one vector to another by an amount `t` defined as a zero to one number.
See: [Matt DesLauriers](https://twitter.com/mattdesl/status/1031305279227478016) has a good thread about this.
================================================
FILE: wiki/donate/README.md
================================================
================================================
FILE: wiki/examples/README.md
================================================
---
pageClass: examples
title: Two.js Examples
lang: en-US
---
# Examples
Various examples of how to use different features of Two.js with associated tags for convenient filtering through the global search bar up at the top. If you've made something with Two.js you'd like to share, then please [submit](https://github.com/jonobr1/two.js/issues/new?assignees=&labels=output&template=output.md&title=%5BOutput%5D) it.
### Official Examples
---
### Community Examples
#### Even More References
There are many more examples that exist out on the internet. Some starting points are:
- [Two.js Collection on Codepen](https://codepen.io/collection/DRdLJk)
- [A Beginner's Guide to Drawing 2D Graphics With Two.js](https://code.tutsplus.com/tutorials/a-beginners-guide-to-drawing-2d-graphics-using-twojs--cms-31681)
- [Drawing and Animating with Two.js and Illustrator](https://modernweb.com/drawing-and-animating-with-two-js-and-illustrator/)
- [Two.js tutor as a ChatGPT bot](https://chatgpt.com/g/g-hkcTX8uPm-two-js-tutor)
================================================
FILE: wiki/incident-response-plan/README.md
================================================
---
pageClass: fine-print
title: Two.js Incident Response Plan
lang: en-US
---
# Incident Response Plan
### Principles:
- **Transparency:** All incidents and fixes are documented here for the community.
- **Stewardship:** Take responsibility for protecting users and the project.
- **Protection:** Act to minimize harm and provide guidance.
# How Two.js Handles Incidents
### 1. Detection & Triage
- We monitor security reports sent via [security](/security) outreach, GitHub advisories, issues, and npm notifications.
- If we spot a bug or report that looks like a security risk, we treat it as an incident.
### 2. Assessment
- Check the severity:
- **Critical:** npm package or repo compromised, malicious code, supply chain attack.
- **High:** Vulnerabilities that allow code execution, XSS, or leak secrets.
- **Medium:** Denial of service, memory leaks, or integrity issues.
- **Low:** Docs defacement, minor regressions.
### 3. Response
- Acknowledge the report (privately if sensitive, publicly if not).
- For critical/high issues:
- Rotate any exposed secrets/tokens.
- Patch the bug or vulnerability.
- Deprecate or yank affected npm versions if needed.
- Rebuild and redeploy docs/site from a clean commit.
- For medium/low issues:
- Patch and document the fix.
### 4. Communication
- Update this wiki page with a summary of the incident and the fix.
- For major issues, we post a GitHub Release note and a pinned Issue.
- Provide upgrade or mitigation steps for users.
### 5. Recovery & Hardening
- After fixing, review what happened and update this process if needed.
- Add tests or automation to prevent similar issues.
- Rotate credentials and check repo/npm security settings.
---
# Recent Incidents & Fixes
- None at this time
---
**If you spot a security issue, please report it via [security](/security) outreach. We’ll respond as quickly as possible.**
================================================
FILE: wiki/privacy/README.md
================================================
# Privacy Policy
- Library: Two.js library code does not collect or send user data.
- Website: This docs site uses Google Analytics (GA4).
- Fonts: Third party fonts are provided by Adobe through Typekit
- Data Collected (website): Page views, browser/device info, approximate location; no user accounts.
- Retention: Analytics event data retained for up to 14 months.
- Data Processors: Google (Analytics). Standard Contractual Clauses apply; see Google’s DPA.
- Your Rights: Access, deletion, portability (where applicable). To exercise rights, contact [inquiries+two.js@jono.fyi](mailto:inquiries+two.js@jono.fyi).
- Contact: [inquiries+two.js@jono.fyi](inquiries+two.js@jono.fyi)
================================================
FILE: wiki/security/README.md
================================================
---
pageClass: fine-print
title: Two.js Security Policy
lang: en-US
---
# Notes on Safety
When using Two.js or any other client-side rendering library we recommend considering:
- Handle untrusted SVGs carefully: Do not load or interpret SVGs from untrusted users without sanitizing. Malicious SVGs can embed external references and scripts. Use an SVG sanitizer and set appropriate `Content-Security-Policy`.
- Images and external assets: Prefer same-origin or vetted hosts. Disable `allow-scripts` in any embedders/iframes and avoid inline event handlers.
- CSP recommended defaults: Consider a CSP that restricts scripts to self and trusted CDNs, disallows inline/eval, and sets `object-src 'none'`.
::: tip Note
The Two.js library does not collect user data. If you embed Two.js in a site that handles user content, apply your own input validation, rate limiting, and abuse reporting workflow.
:::
# Security Policy
If you have discovered a security vulnerability in this project, please report it
privately. **Do not disclose it as a public issue.** This gives us time to work with you to fix the issue before public exposure, reducing the chance that the exploit will be used before a patch is released.
**You may submit the report in the following ways:**
- Github users can privately report security advisories directly [here](https://github.com/jonobr1/two.js/security/advisories/new)
- Send an email to [inquiries+two.js@jono.fyi](mailto:inquiries+two.js@jono.fyi).
**Please provide the following information in your report:**
- The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting)
- Full paths of source file(s) related to the manifestation of the issue
- The location of the affected source code (tag/branch/commit or direct URL)
- Any special configuration required to reproduce the issue
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue, including how an attacker might exploit the issue
This project is maintained by volunteers on a reasonable-effort basis. As such, we ask that you give us 90 days to work on a fix before public exposure.
---
_Two.js conforms to this [Incident Response Plan](https://two.js.org/incident-response-plan) in moments of security risks._