Repository: konvajs/konva
Branch: master
Commit: 811d62a3f458
Files: 162
Total size: 1.9 MB
Directory structure:
gitextract_6dbjwh6u/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE.md
│ └── workflows/
│ ├── build.yml
│ ├── prettier.yml
│ ├── release.yml
│ ├── test-browser.yml
│ └── test-node.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── gulpfile.mjs
├── package.json
├── release.sh
├── resources/
│ ├── doc-includes/
│ │ ├── ContainerParams.txt
│ │ ├── NodeParams.txt
│ │ └── ShapeParams.txt
│ └── jsdoc.conf.json
├── rollup.config.mjs
├── src/
│ ├── Animation.ts
│ ├── BezierFunctions.ts
│ ├── Canvas.ts
│ ├── Container.ts
│ ├── Context.ts
│ ├── Core.ts
│ ├── DragAndDrop.ts
│ ├── Factory.ts
│ ├── FastLayer.ts
│ ├── Global.ts
│ ├── Group.ts
│ ├── Layer.ts
│ ├── Node.ts
│ ├── PointerEvents.ts
│ ├── Shape.ts
│ ├── Stage.ts
│ ├── Tween.ts
│ ├── Util.ts
│ ├── Validators.ts
│ ├── _CoreInternals.ts
│ ├── _FullInternals.ts
│ ├── canvas-backend.ts
│ ├── filters/
│ │ ├── Blur.ts
│ │ ├── Brighten.ts
│ │ ├── Brightness.ts
│ │ ├── Contrast.ts
│ │ ├── Emboss.ts
│ │ ├── Enhance.ts
│ │ ├── Grayscale.ts
│ │ ├── HSL.ts
│ │ ├── HSV.ts
│ │ ├── Invert.ts
│ │ ├── Kaleidoscope.ts
│ │ ├── Mask.ts
│ │ ├── Noise.ts
│ │ ├── Pixelate.ts
│ │ ├── Posterize.ts
│ │ ├── RGB.ts
│ │ ├── RGBA.ts
│ │ ├── Sepia.ts
│ │ ├── Solarize.ts
│ │ └── Threshold.ts
│ ├── index.ts
│ ├── shapes/
│ │ ├── Arc.ts
│ │ ├── Arrow.ts
│ │ ├── Circle.ts
│ │ ├── Ellipse.ts
│ │ ├── Image.ts
│ │ ├── Label.ts
│ │ ├── Line.ts
│ │ ├── Path.ts
│ │ ├── Rect.ts
│ │ ├── RegularPolygon.ts
│ │ ├── Ring.ts
│ │ ├── Sprite.ts
│ │ ├── Star.ts
│ │ ├── Text.ts
│ │ ├── TextPath.ts
│ │ ├── Transformer.ts
│ │ └── Wedge.ts
│ ├── skia-backend.ts
│ └── types.ts
├── test/
│ ├── assets/
│ │ ├── tiger.ts
│ │ └── worldMap.ts
│ ├── bunnies.html
│ ├── ifame.html
│ ├── import-test.cjs
│ ├── import-test.mjs
│ ├── manual/
│ │ ├── Blur-test.ts
│ │ ├── Brighten-test.ts
│ │ ├── Contrast-test.ts
│ │ ├── Emboss-test.ts
│ │ ├── Enhance-test.ts
│ │ ├── Grayscale-test.ts
│ │ ├── HSL-test.ts
│ │ ├── HSV-test.ts
│ │ ├── Invert-test.ts
│ │ ├── Kaleidoscope-test.ts
│ │ ├── Manual-test.ts
│ │ ├── Mask-test.ts
│ │ ├── Noise-test.ts
│ │ ├── Pixelate-test.ts
│ │ ├── Posterize-test.ts
│ │ ├── RGB-test.ts
│ │ ├── RGBA-test.ts
│ │ ├── Sepia-test.ts
│ │ ├── Solarize-test.ts
│ │ └── Threshold-test.ts
│ ├── manual-tests.html
│ ├── node-canvas-global-setup.mjs
│ ├── node-skia-global-setup.mjs
│ ├── performance/
│ │ ├── bunnies_native.html
│ │ ├── creating_elements.html
│ │ └── jump-shape.html
│ ├── runner.js
│ ├── sandbox.html
│ ├── text-paths.html
│ ├── typescript/
│ │ └── event-delegation-test.ts
│ ├── unit/
│ │ ├── Animation-test.ts
│ │ ├── Arc-test.ts
│ │ ├── Arrow-test.ts
│ │ ├── AutoDraw-test.ts
│ │ ├── Blob-test.ts
│ │ ├── Canvas-test.ts
│ │ ├── Circle-test.ts
│ │ ├── Container-test.ts
│ │ ├── Context-test.ts
│ │ ├── DragAndDrop-test.ts
│ │ ├── DragAndDropEvents-test.ts
│ │ ├── Ellipse-test.ts
│ │ ├── Filter-test.ts
│ │ ├── Global-test.ts
│ │ ├── Group-test.ts
│ │ ├── Image-test.ts
│ │ ├── Label-test.ts
│ │ ├── Layer-test.ts
│ │ ├── Line-test.ts
│ │ ├── MouseEvents-test.ts
│ │ ├── Node-cache-test.ts
│ │ ├── Node-test.ts
│ │ ├── Path-test.ts
│ │ ├── PointerEvents-test.ts
│ │ ├── Polygon-test.ts
│ │ ├── Rect-test.ts
│ │ ├── RegularPolygon-test.ts
│ │ ├── Ring-test.ts
│ │ ├── Shape-test.ts
│ │ ├── Spline-test.ts
│ │ ├── Sprite-test.ts
│ │ ├── Stage-test.ts
│ │ ├── Star-test.ts
│ │ ├── Text-test.ts
│ │ ├── TextPath-test.ts
│ │ ├── TouchEvents-test.ts
│ │ ├── Transformer-test.ts
│ │ ├── Tween-test.ts
│ │ ├── Util-test.ts
│ │ ├── Wedge-test.ts
│ │ ├── imagediff.ts
│ │ └── test-utils.ts
│ └── unit-tests.html
├── tsconfig.json
└── tsconfig.test.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [lavrton]
patreon: lavrton
open_collective: konva
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
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
Thank you for submitting an issue!
Please make sure to check current open and closed issues to see if your question has been asked or answered before.
If you have just a question (not a bug or a feature request) it is better to ask it in [Stackoverflow](http://stackoverflow.com/questions/tagged/konvajs).
If you have a bug, please, try to create a reproducible example with jsfiddle (or any similar service).
You can use [this JSBIN](https://jsbin.com/necojavuma/edit?js,output) as a template.
================================================
FILE: .github/workflows/build.yml
================================================
name: Build
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [23.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run build
================================================
FILE: .github/workflows/prettier.yml
================================================
name: check formatting
on:
pull_request:
jobs:
fmt-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: 'latest'
- run: npm install
- run: npm run fmt:check
================================================
FILE: .github/workflows/release.yml
================================================
---
name: 'tagged-release'
on:
push:
tags:
- '*'
jobs:
tagged-release:
name: 'Tagged Release'
runs-on: 'ubuntu-latest'
steps:
# ...
- name: 'Build & test'
run: |
echo "done!"
- uses: 'marvinpinto/action-automatic-releases@latest'
with:
repo_token: '${{ secrets.GITHUB_TOKEN }}'
prerelease: false
================================================
FILE: .github/workflows/test-browser.yml
================================================
name: Test Browser
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run test:browser
================================================
FILE: .github/workflows/test-node.yml
================================================
name: Test NodeJS
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [23.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run test:node
================================================
FILE: .gitignore
================================================
dist
es
.parcel-cache
test-build
documentation
analysis
node_modules
bower_components
phantomjs.exe
docs
homedocs
jsdoc-template
api
package-lock.json
lib
src_old
*.zip
*_cache
types
out.png
cmj
.test-temp
.history
.claude
# Numerous always-ignore extensions
*.diff
*.err
*.orig
*.log
*.rej
*.swo
*.swp
*.vi
*~
*.sass-cache
# OS or Editor folders
.DS_Store
Thumbs.db
.cache
.project
.settings
.tmproj
*.esproj
nbproject
*.sublime-project
*.sublime-workspace
*.md.html
.vscode
# Dreamweaver added files
_notes
dwsync.xml
# Komodo
*.komodoproject
.komodotools
# Folders to ignore
.hg
.svn
.CVS
intermediate
publish
.idea
konva.js
konva.min.js
================================================
FILE: CHANGELOG.md
================================================
# Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## 10.2.3 (2026-03-16)
- More crash fixes
## 10.2.1 (2026-03-13)
- Fix possible crash
## 10.2.0 (2026-01-15)
- Added `rotateAnchorAngle` property to `Transformer` to control the position of the rotation anchor around the bounding box
## 10.1.0 (2026-01-14)
- Added underline offset option and related test for Text Annotation
- Fixed large memory usage on cache
- Fix bounding box calculation for bezier lines
- Fixed cached render with buffer canvas is used
## 10.0.12 (2025-11-21)
- Better canvas farbling detection logic
## 10.0.11 (2025-11-20)
- Fixed broken release
## 10.0.10 (2025-11-20)
- Update hit detection system to handle canvas farbling. Hit detection should work better on Brave browser. Thanks [@wiverson](https://github.com/wiverson) to the idea and implementation idea.
## 10.0.9 (2025-11-08)
- Fixed line-through rendering when letter spacing is used
## 10.0.8 (2025-10-24)
- Fixed opacity level when a cached shape has opacity, fill and stroke
## 10.0.7 (2025-10-22)
- Fixed image element size re-calculation when change is changed with transformer is used.
## 10.0.6 (2025-10-22)
- Better `Image.getClientRect()` calculation if an instance has no image attached yet
## 10.0.5 (2025-10-22)
- Simplify types to fix TS errors
## 10.0.4 (2025-10-21)
- Remove logs
## 10.0.3 (2025-10-21)
- Add text decoration options to TextPath: support for 'line-through' and combined styles with 'underline'
## 10.0.2 (2025-09-10)
- Fixed internal calculations for `TextPath` to return correct width and height
## 10.0.1 (2025-09-09)
- Fixed `line-through` render for center/right aligned text
## 10.0.0 (2025-09-07)
### Breaking Changes
- **Breaking**: Konva module is fully migrated from CommonJS modules to ES modules. It may break some older bundlers and CommonJS environments. In CommonJS environment you have to use default property from require:
```js
// before
const Konva = require('konva');
// after
const Konva = require('konva').default;
```
- **Breaking:** Dropped default support for node.js environment. Now you have to explicitly import it:
```bash
npm install canvas
```
```js
import Konva from 'konva';
import 'konva/canvas-backend';
```
Motivation: With increased usage of `konva` in SSR environments like Next.js, loading native canvas rendering on the server is unnecessary since we don't render canvas content server-side. Removing this requirement simplifies setup by avoiding native modules when they aren't needed.
- Improved text positioning to match DOM/CSS rendering. To restore previous behaviour use `Konva.legacyTextRendering = true`. This should NOT break major part of the apps. But if you care about pixel-perfect position of text elements, that change may effect you.
### New Features
- Added new `skia` render backend for node.js:
```bash
npm install skia-canvas
```
```js
import Konva from 'konva';
import 'konva/skia-backend';
```
- Native filters support via `node.filters(['blur(10px)'])`. Native fitlers works MUCH faster if supported nativily (Chrome, Firefox). If there is no native support, Konva will automatially fallback to functional filter (on Safari).
```js
node.filters(['blur(10px')]);
node.cache();
```
- New property `charRenderFunc` for `Konva.Text` for controlling "per-character-render". May be useful any character animations:
```js
var text = new Konva.Text({
x: 10,
y: 10,
text: 'AB',
fontSize: 20,
charRenderFunc: function ({ context, index }) {
if (index === 1) {
// shift only the second character
context.translate(0, 10);
}
},
});
```
- **New**: Added `Konva.Filters.Brightness` filter in replace of deprecated `Konva.Filters.Brighten` to better match with css filters logic.
- Added `cornerRadius` support for `Konva.RegularPolygon`
- Added `miterLimit` property support for `Konva.Shape` to control line join appearance
### Bug Fixes
- Fixed corner radius render for `Konva.Rect` when negative width or height are used
- Fixed TextPath rendering on right align for some fonts
- Fixed crash when node inside transformer was destroyed
- Fixed mouseup + click events order when clicked on empty area of stage
- Fixed transformer drag behavior with non-draggable nodes
### Technical Improvements
- **Performance**: Rewrote Emboss and Solarize filters for improved performance and usability
- Changed return type of `node.toImage()`
- Brave detection and warning
## 9.3.22 (2025-07-08)
- Fixed possible crash on `node.to()` method
## 9.3.21 (2025-07-07)
- Fixed memory leaks on Tween destroy
- Fixed incorrect export of stage/layer when internal nodes used buffer canvas for rendering
- Fixed incorrect render of cached node when buffer canvas is used
- Fixed incorrect path lenth calculations
- Fixed `pointerleave` bubbling
- Added `pointerleave` event in `Stage`
## 9.3.20 (2025-03-20)
- Fix text rendering when ellipses are used
## 9.3.19 (2025-03-12)
- Typescript fixes
- Memory leak fixes
## 9.3.18 (2024-12-23)
- Fixed emoji split in multiple lines
## 9.3.17 (2024-12-23)
- Fixed `Arrow.getClientRect()`
- Fixed emoji rendering with letterSpacing
- Fixed line-through for justify text
- Changes in letter spacing width calculations to match DOM rendering
## 9.3.16 (2024-10-21)
- Fix freeze on ios on touch cancel event
- Typescript fixes
## 9.3.15 (2024-09-09)
- fix letter spacing for Hindi text
- ts fixes
### 9.3.14 (2024-07-16)
- Fix shadow + corner radius for images
- Support `fillRule` for `Konva.Shape` on hit graph
### 9.3.13 (2024-07-05)
- Fallback for `Konva.Text.measureSize()` when 2d context doesn't return full data
### 9.3.12 (2024-06-20)
- Fix stopped transforming when it was triggered by multi-touch
- Fix svg `path.getPointAtLength()` calculations in some cases
- Fix `shape.getClientRect()` when any of parents is cached
### 9.3.11 (2024-05-23)
- Fix chrome clear canvas issue
- Typescript fixes
### 9.3.9 (2024-05-20)
- Fix underline and line-through for `Konva.Text` when `Konva._fixTextRendering = true`
### 9.3.8 (2024-05-15)
- Fix click events fires on stage
- Temporary `Konva._fixTextRendering = true` flag to fix inconsistent text
### 9.3.6 (2024-03-04)
- Fix transformer bug to enable hit graph back
### 9.3.5 (2024-03-04)
- `tranformer` event will be triggered AFTER all data of transformer is updated
- Improve performance of transformer
### 9.3.4 (2024-03-03)
- Fix clipping with zero size
### 9.3.3 (2024-02-09)
- Another fix for exporting buffered shapes
### 9.3.2 (2024-01-26)
- Fix large memory usage on node export
### 9.3.1 (2024-01-17)
- Fix Pixelate filter work/fix caching size
- Fix node export when large buffer canvas is used
### 9.3.0 (2023-12-20)
- New attribute `rotateLineVisible` for `Konva.Transformer` to show/hide rotate line
### 9.2.3 (2023-10-31)
- Better `Konva.Transformer` work when it has `flipEnabled = false`.
### 9.2.2 (2023-09-14)
- Better RTL support
- Some typescript fixes
### 9.2.1 (2023-09-14)
- Fix text rendering when text has both underline and shadow
- Typescript fixes
### 9.2.0 (2023-05-14)
- More controls on clipping
- `fillRule` for `Konva.Shape`
### 9.1.0 (2023-05-14)
- New `anchorStyleFunc` for `Konva.Transformer` to customize anchor style
### 9.0.2 (2023-05-14)
- Better text rendering when it has stroke
### 9.0.1 (2023-04-17)
- Better performance for any instance creation
- Little typescript fixes
### 9.0.0 (2023-04-13)
- Migrate the npm package from ES back to CommonJS
### 8.4.4 (2023-04-05)
- Some fixes for `Konva.TextPath` calculations and rendering.
- Resolve "willReadFrequently" warning in Chrome
### 8.4.3 (2023-03-23)
- Typescript fixes
- Better validation for `Konva.Transfomer` `nodes` property
### 8.4.2 (2023-01-20)
- Fix justify on text with limited height
### 8.4.1 (2023-01-19)
- Typescript fixes for `container.add()` method. Ability to use empty array as argument. E.g. `container.add(...emptyArray)`
- Fix underline for justify text
- Fix gradient display on underline or line-through text
### 8.4.0 (2023-01-05)
- Add support for `cornerRadius` for Konva.Image
- Fix cloning of `Konva.Transformer`
### 8.3.14 (2022-11-09)
- Automatically release (destroy) used canvas elements. Should fix safari memory issues
### 8.3.13 (2022-10-03)
- Typescript fixes
- Better non-passive events usage
- Better 2d context usage to avoid Chrome warnings
### 8.3.12 (2022-08-29)
- `ellipsis` fixes for `Konva.Text`
- Allow reset component attributes via overloader
### 8.3.11 (2022-08-05)
- Fix `Konva.Label` position when tag attributes are changed
- Fix incorrect ellipsis display for `Konva.Text`
- Fix `click` event trigger on parent containers on touch devices
- Fix incorrect `mouseleave` event trigger when drag is finished
### 8.3.10 (2022-06-20)
- Skip `Konva.Transformer` in `container.getClientRect()` calculations
### 8.3.9 (2022-05-27)
- Typescript fixes
### 8.3.8 (2022-05-05)
- Disable all exports in `package.json`
### 8.3.7 (2022-05-04)
- Migrate to CommonJS exports only
### 8.3.6 (2022-04-27)
- Better exports definitions. Importing `Konva` should work better in different bundlers and test environments.
- `imageSmoothingEnabled` option for `node.toDataURL()`, `node.toCanvas()` and `node.toImage()`
## 8.3.5 (2022-03-21)
- Quick fix for `toCanvas()` and `toDataURL()` size calculation.
## 8.3.4 (2022-03-13)
- Fix characters positions calculations on `fontFamily` changes in `TextPath`.
- Remove rounding in `node.getClientRect()` results
- Fix event object on `transformstart` event.
## 8.3.3 (2022-02-23)
- Fix `justify` align for text with several paragraphs.
## 8.3.2
- Remove source maps for webpack builds
## 8.3.1 (2021-12-09)
- Fix `dbltap` event in Safari
- A bit faster `node.moveToTop()` when node is already on top
- Better client rect calculations for `Konva.Arc` shape.
## 8.3.0 (2021-11-15)
- new `transformer.anchorDragBoundFunc` method.
## 8.2.4 (2021-11-15)
- Fix not working `Konva.Transformer` when several transformers were used
## 8.2.2
- Fix `Konva.Arrow` rendering when it has two pointers
## 8.2.1
- Fix `package.json` exports.
## 8.2.0
- Restore build in CommonJS. `const Konva = require('konva/cmj').default;`
- Fix arrow rendering when dash is used
- Fix `dbltap` trigger when multi-touch is used
## 8.1.4
- Fix `dblclick` event when `cancelBubble` is used.
## 8.1.3
- Fix `fillPattern` cache invalidation on shapes
## 8.1.2
- Fix memory leak for `Konva.Image`
## 8.1.1
- Fix `Konva.Transformer` dragging draw when `shouldOverdrawWholeArea = true`.
- Fix auto redraw when `container.removeChildren()` or `container.destroyChildren()` are used
## 8.1.0
- New property `useSingleNodeRotation` for `Konva.Transformer`.
## 8.0.4
- Fix fill pattern updates on `fillPatternX` and `fillPatternY` changes.
## 8.0.2
- Fix some transform caches
- Fix cache with hidden shapes
## 8.0.1
- Some typescript fixes
## 8.0.0
This is a very large release! The long term of `Konva` API is to make it simpler and faster. So when possible I am trying to optimize the code and remove unpopular/confusing API methods.
**BREAKING:**
- `Konva.Collection` is removed. `container.children` is a simple array now. `container.find()` will returns an array instead of `Konva.Collection()` instance.
`Konva.Collection` was confusing for many users. Also it was slow and worked with a bit of magic. So I decided to get rif of it. Now we are going to use good old arrays.
```js
// old code:
group.find('Shape').visible(false);
// new code:
group.find('Shape').forEach((shape) => shape.visible(false));
```
- argument `selector` is removed from `node.getIntersection(pos)` API. I don't think you even knew about it.
- `Konva.Util.extend` is removed.
- All "content" events from `Konva.Stage` are removed. E.g. instead of `contentMousemove` just use `mousemove` event.
**New features:**
- All updates on canvas will do automatic redraw with `layer.batchDraw()`. This features is configurable with `Konva.autoDrawEnabled` property. Konva will automatically redraw layer when you change any property, remove or add nodes, do caching. So you don't need to call `layer.draw()` or `layer.batchDraw()` in most of the cases.
- New method `layer.getNativeCanvasElement()`
- new `flipEnabled` property for `Konva.Transformer`
- new `node.isClientRectOnScreen()` method
- Added `Konva.Util.degToRad` and `Konva.Util.radToDeg`
- Added `node.getRelativePointerPosition()`
**Changes and fixes:**
- **Full migration to ES modules package (!), commonjs code is removed.**
- **`konva-node` is merged into `konva` npm package. One package works for both environments.**
- Full event system rewrite. Much better `pointer` events support.
- Fix `TextPath` recalculations on `fontSize` change
- Better typescript support. Now every module has its own `*.d.ts` file.
- Removed `Konva.UA`, `Konva._parseUA` (it was used for old browser detection)
- Fixed Arrow head position when an arrow has tension
- `textPath.getKerning()` is removed
- Fix `a` command parsing for `Konva.Path`
- Fix fill pattern for `Konva.Text` when the pattern has an offset or rotation
- `Konva.names` and `Konva.ids` are removed
- `Konva.captureTouchEventsEnabled` is renamed to `Konva.capturePointerEventsEnabled`
## 7.2.5
- Fix transform update on `letterSpacing` change of `Konva.Text`
## 7.2.4
- Fix wrong `mouseleave` trigger for `Konva.Stage`
## 7.2.3
- Fix transformer rotation when parent of a node is rotated too.
## 7.2.2
- Fix wrong size calculations for `Konva.Line` with tension
- Fix `shape.intersects()` behavior when a node is dragged
- Fix ellipsis rendering for `Konva.Text`
## 7.2.1
- Fix correct rendering of `Konva.Label` when heigh of text is changed
- Fix correct `transformstart` and `transformend` events when several nodes are attached with `Konva.Transformer`
## 7.2.0
- New property `fillAfterStrokeEnabled` for `Konva.Shape`. See API docs for more information.
- Fix for `Konva.Transformer` when it may fail to draw.
- Fix rendering of `TextPath` one more time.
## 7.1.9
- Fix autodrawing for `Konva.Transformer` when it is on a different layer
- Fix `Konva.RegularPolygon` size calculations.
## 7.1.8
- Fix incorrect rendering of `TextPath` in some cases. (again)
## 7.1.7
- Fix incorrect rendering of `TextPath` in some cases.
## 7.1.6
- Fix for correct image/dataURL/canvas exports for `Konva.Stage`.
## 7.1.5
- Performance fixes for dragging many nodes with `Konva.Transformer`.
- Documentation updates
## 7.1.4
- Perf fixes
- Change events trigger flow, so adding new events INSIDE event callback will work correctly.
- Fix double `dragend`, `dragstart`, `dragmove` triggers on `Konva.Transformer`
## 7.1.3
- Text rendering fixes
## 7.1.2
- fix ellipses behavior for `Konva.Text`.
- fix scaled fill pattern for text.
## 7.1.1
- fixes for `dragstart` event when `Konva.Transformer` is used. `dragstart` event will have correct native `evt` reference
- Better unicode support in `Konva.Text` and `Konva.TextPath`. Emoji should work better now 👍
## 7.1.0
- Multi row support for `ellipsis` config for `Konva.Text`
- Better `Konva.Transfomer` behavior when single attached node is programmatically rotated.
## 7.0.7
- fixes for `dragstart` event when `Konva.Transformer` is used. `dragstart` will not bubble from transformer.
- `string` and `fill` properties validation can accept `CanvasGradient` as valid value
## 7.0.6
- Better performance for stage dragging
## 7.0.5
- Fixes for `node.cache()` function.
## 7.0.4
- Add `onUpdate` callbacks to `Konva.Tween` configuration and `node.to()` method.
- Up to 6x faster initializations of objects, like `const shape = new Konva.Shape()`.
## 7.0.3 - 2020-07-09
- Fix wring `dragend` trigger on `draggable` property change inside `click`
- Fix incorrect text rendering with `letterSpacing !== 0`
- Typescript fixes
## 7.0.2 - 2020-06-30
- Fix wrong trigger `dbltap` and `click` on mobile
## 7.0.1 - 2020-06-29
- Fixes for different font families support.
- Fixes for `Konva.Transformer` positions
- Types fixes for better Typescript support
## 7.0.0 - 2020-06-23
- **BREAKING** `inherit` option is removed from `visible` and `listening`. They now just have boolean values `true` or `false`. If you do `group.listening(false);` then whole group and all its children will be removed from the hitGraph (and they will not listen to events). Probably 99% `Konva` applications will be not affected by this _breaking change_.
- **Many performance fixes and code size optimizations. Up to 70% performance boost for many moving nodes.**
- `layer.hitGraphEnabled()` is deprecated. Just use `layer.listening(false)` instead
- Better support for font families with spaces inside (like `Font Awesome 5`).
- Fix wrong `dblclick` and `dbltap` triggers
- Deprecate `Konva.FastLayer`. Use `new Konva.Layer({ listening: false });` instead.
- `dragmove` event will be fired on `Konva.Transformer` too when you drag a node.
- `dragmove` triggers only after ALL positions of dragging nodes are changed
## 6.0.0 - 2020-05-08
- **BREAKING!** `boundBoxFunc` of `Konva.Transformer` works in absolute coordinates of whole transformer. Previously in was working in local coordinates of transforming node.
- Many `Konva.Transformer` fixes. Now it works correctly when you transform several rotated shapes.
- Fix for wrong `mouseleave` and `mouseout` fire on shape remove/destroy.
## 5.0.3 - 2020-05-01
- Fixes for `boundBoxFunc` of `Konva.Transformer`.
## 5.0.2 - 2020-04-23
- Deatach fixes for `Konva.Transformer`
## 5.0.1 - 2020-04-22
- Fixes for `Konva.Transformer` when parent scale is changed
- Fixes for `Konva.Transformer` when parent is draggable
- Performance optimizations
## 5.0.0 - 2020-04-21
- **New `Konva.Transformer` implementation!**. Old API should work. But I marked this release is `major` (breaking) just for smooth updates. Changes:
- Support of transforming multiple nodes at once: `tr.nodes([shape1, shape2])`.
- `tr.node()`, `tr.setNode()`, `tr.attachTo()` methods are deprecated. Use `tr.nodes(array)` instead
- Fixes for center scaling
- Fixes for better `padding` support
- `Transformer` can be placed anywhere in the tree of a stage tree (NOT just inside a parent of attached node).
- Fix `imageSmoothEnabled` resets when stage is resized
- Memory usage optimizations when a node is cached
## 4.2.2 - 2020-03-26
- Fix hit stroke issues
## 4.2.1 - 2020-03-26
- Fix some issues with `mouseenter` and `mouseleave` events.
- Deprecate `hitStrokeEnabled` property
- Fix rounding issues for `getClientRect()` for some shapes
## 4.2.0 - 2020-03-14
- Add `rotationSnapTolerance` property to `Konva.Transformer`.
- Add `getActiveAnchor()` method to `Konva.Transformer`
- Fix hit for non-closed `Konva.Path`
- Some fixes for experimental Offscreen canvas support inside a worker
## 4.1.6 - 2020-02-25
- Events fixes for `Konva.Transformer`
- Now `Konva` will keep `id` in a cloned node
- Better error messages on tainted canvas issues
## 4.1.5 - 2020-02-16
- Fixes for `path.getClientRect()` function calculations
## 4.1.4 - 2020-02-10
- Fix wrong internal caching of absolute attributes
- Fix `Konva.Transformer` behavior on scaled with CSS stage
## 4.1.3 - 2020-01-30
- Fix line with tension calculations
- Add `node.getAbsoluteRotation()` method
- Fix cursor on anchors for rotated parent
## 4.1.2 - 2020-01-08
- Fix possible `NaN` in content calculations
## 4.1.1 - 2020-01-07
- Add ability to use `width = 0` and `height = 0` for `Konva.Image`.
- Fix `cache()` method of `Konva.Arrow()`
- Add `Transform` to `Konva` default exports. So `Konva.Transform` is available now.
## 4.1.0 - 2019-12-23
- Make events work on some CSS transforms
- Fix caching on float dimensions
- Fix `mouseleave` event on stage.
- Increase default anchor size for `Konva.Transformer` on touch devices
## 4.0.18 - 2019-11-20
- Fix `path.getClientRect()` calculations for `Konva.Path`
- Fix wrong fire of `click` and `tap` events on stopped drag events.
## 4.0.17 - 2019-11-08
- Allow hitStrokeWidth usage, even if a shape has not stroke visible
- Better IE11 support
## 4.0.16 - 2019-10-21
- Warn on undefined return value of `dragBoundFunc`.
- Better calculations for `container.getClientRect()`
## 4.0.15 - 2019-10-15
- TS fixes
- Better calculations for `TextPath` with align = right
- Better `textPath.getClientRect()`
## 4.0.14 - 2019-10-11
- TS fixes
- Fix globalCompositeOperation + cached hit detections.
- Fix absolute position calculations for cached parent
## 4.0.13 - 2019-10-02
- Fix `line.getClientRect()` calculations for line with a tension or low number of points
## 4.0.12 - 2019-09-17
- Fix some bugs when `Konva.Transformer` has `padding > 0`
## 4.0.10 - 2019-09-10
- Fix drag position handling
- Fix multiple selector for find() method
## 4.0.9 - 2019-09-06
- Fix `Konva.Transformer` behavior on mirrored nodes
- Fix `stage.getPointerPosition()` logic.
## 4.0.8 - 2019-09-05
- Fix `dragend` event on click
- Revert fillPatternScale for text fix.
## 4.0.7 - 2019-09-03
- Fixed evt object on `dragstart`
- Fixed double tap trigger after dragging
## 4.0.6 - 2019-08-31
- Fix fillPatternScale for text
## 4.0.5 - 2019-08-17
- Fix `dragstart` flow when `node.startDrag()` is called.
- Fix `tap` and `dbltap` double trigger on stage
## 4.0.4 - 2019-08-12
- Add `node.isCached()` method
- Fix nested dragging bug
## 4.0.3 - 2019-08-08
- Slightly changed `mousemove` event flow. It triggers for first `mouseover` event too
- Better `Konva.hitOnDragEnabled` support for mouse inputs
## 4.0.2 - 2019-08-08
- Fixed `node.startDrag()` behavior. We can call it at any time.
## 4.0.1 - 2019-08-07
- Better `Konva.Arrow` + tension drawing
- Typescript fixes
## 4.0.0 - 2019-08-05
Basically the release doesn't have any breaking changes. You may only have issues if you are using something from `Konva.DD` object (which is private and never documented). Otherwise you should be fine. `Konva` has major upgrade about touch events system and drag&drop flow. The API is exactly the same. But the internal refactoring is huge so I decided to make a major version. Please upgrade carefully. Report about any issues you have.
- Better multi-touch support. Now we can trigger several `touch` events on one or many nodes.
- New drag&drop implementation. You can drag several shapes at once with several pointers.
- HSL colors support
## 3.4.1 - 2019-07-18
- Fix wrong double tap trigger
## 3.4.0 - 2019-07-12
- TS types fixes
- Added support for different values for `cornerRadius` of `Konva.Rect`
## 3.3.3 - 2019-06-07
- Some fixes for better support `konva-node`
- TS types fixes
## 3.3.2 - 2019-06-03
- TS types fixes
## 3.3.1 - 2019-05-28
- Add new property `imageSmoothingEnabled` to the node caching
- Even more ts fixes. Typescript need a lot of attention, you know...
## 3.3.0 - 2019-05-28
- Enable strict mode for ts types
- Add new property `imageSmoothingEnabled` to the layer
## 3.2.7 - 2019-05-27
- Typescript fixes
- Experimental pointer events support. Do `Konva._pointerEventsEnabled = true;` to enable
- Fix some `Konva.Transformer` bugs.
## 3.2.6 - 2019-05-09
- Typescript fixes again
## 3.2.5 - 2019-04-17
- Show a warning when `Konva.Transformer` and attaching node have different parents.
- Typescript fixes
## 3.2.4 - 2019-04-05
- Fix some stage events. `mouseenter` and `mouseleave` should work correctly on empty spaces
- Fix some typescript types
- Better detection of production mode (no extra warnings)
## 3.2.3 - 2019-03-21
- Fix `hasName` method for empty name cases
## 3.2.2 - 2019-03-19
- Remove `dependencies` from npm package
## 3.2.1 - 2019-03-18
- Better `find` and `findOne` lookup. Now we should not care about duplicate ids.
- Better typescript definitions
## 3.2.0 - 2019-03-10
- new property `shape.hitStrokeWidth(10)`
- Better typescript definitions
- Remove `Object.assign` usage (for IE11 support)
## 3.1.7 - 2019-03-06
- Better modules and TS types
## 3.1.6 - 2019-02-27
- Fix commonjs exports
- Fix global injections
## 3.1.0 - 2019-02-27
- Make `Konva` modular: `import Konva from 'konva/lib/Core';`;
- Fix incorrect `Transformer` behavior
- Fix drag&drop for touch devices
## 3.0.0 - 2019-02-25
## Breaking
Customs builds are temporary removed from npm package. You can not use `import Konva from 'konva/src/Core';`.
This feature will be added back later.
### Possibly breaking
That changes are private and internal specific. They should not break most of `Konva` apps.
- `Konva.Util.addMethods` is removed
- `Konva.Util._removeLastLetter` is removed
- `Konva.Util._getImage` is removed
- `Konv.Util._getRGBAString` is removed
- `Konv.Util._merge` is removed
- Removed polyfill for `requestAnimationFrame`.
- `id` and `name` properties defaults are empty strings, not `undefined`
- internal `_cache` property was updated to use es2015 `Map` instead of `{}`.
- `Konva.Validators` is removed.
### Added
- Show a warning when a stage has too many layers
- Show a warning on duplicate ids
- Show a warning on weird class in `Node.create` parsing from JSON
- Show a warning for incorrect value for component setters.
- Show a warning for incorrect value for `zIndex` property.
- Show a warning when user is trying to reuse destroyed shape.
- new publish method `measureSize(string)` for `Konva.Text`
- You can configure what mouse buttons can be used for drag&drop. To enable right button you can use `Konva.dragButtons = [0, 1]`.
- Now you can hide stage `stage.visible(false)`. It will set its container display style to "none".
- New method `stage.setPointersPositions(event)`. Usually you don't need to use it manually.
- New method `layer.toggleHitCanvas()` to show and debug hit areas
### Changed
- Full rewrite to Typescript with tons of refactoring and small optimizations. The public API should be 100% the same
- Fixed `patternImage` and `radialGradient` for `Konva.Text`
- `Konva.Util._isObject` is renamed to `Konva.Util._isPlainObject`.
- A bit changed behavior of `removeId` (private method), now it doesn't clear node ref, if object is changed.
- simplified `batchDraw` method (it doesn't use `Konva.Animation`) now.
- Performance improvements for shapes will image patterns, linear and radial fills
- `text.getTextHeight()` is deprecated. Use `text.height()` or `text.fontSize()` instead.
- Private method `stage._setPointerPosition()` is deprecated. Use `stage.setPointersPositions(event)`;
### Fixed
- Better mouse support on mobile devices (yes, that is possible to connect mouse to mobile)
- Better implementation of `mouseover` event for stage
- Fixed underline drawing for text with `lineHeight !== 1`
- Fixed some caching behavior when a node has `globalCompositeOperation`.
- Fixed automatic updates for `Konva.Transformer`
- Fixed container change for a stage.
- Fixed warning for `width` and `height` attributes for `Konva.Text`
- Fixed gradient drawing for `Konva.Text`
- Fixed rendering with `strokeWidth = 0`
## 2.6.0 - 2018-12-14
### Changed
- Performance fixes when cached node has many children
- Better drawing for shape with `strokeScaleEnabled = false` on HDPI devices
### Added
- New `ignoreStroke` for `Konva.Transformer`. Good to use when a shape has `strokeScaleEnabled = false`
### Changed
- `getKerning` TextPath API is deprecated. Use `kerningFunc` instead.
## 2.5.1 - 2018-11-08
### Changed
- Use custom functions for `trimRight` and `trimLeft` (for better browsers support)
## 2.5.0 - 2018-10-24
### Added
- New `anchorCornerRadius` for `Konva.Transformer`
### Fixed
- Performance fixes for caching
### Changed
- `dragstart` event behavior is a bit changed. It will fire BEFORE actual position of a node is changed.
## 2.4.2 - 2018-10-12
### Fixed
- Fixed a wrong cache when a shape inside group has `listening = false`
## 2.4.1 - 2018-10-08
### Changed
- Added some text trim logic to wrap in better
### Fixed
- `getClientRect` for complex paths fixes
- `getClientRect` calculation fix for groups
- Update `Konva.Transformer` on `rotateEnabled` change
- Fix click stage event on dragend
- Fix some Transformer cursor behavior
## 2.4.0 - 2018-09-19
### Added
- Centered resize with ALT key for `Konva.Transformer`
- New `centeredScaling` for `Konva.Transformer`
### Fixed
- Tween support for gradient properties
- Add `user-select: none` to the stage container to fix some "selected contend around" issues
## 2.3.0 - 2018-08-30
### Added
- new methods `path.getLength()` and `path.getPointAtLength(val)`
- `verticalAlign` for `Konva.Text`
## 2.2.2 - 2018-08-21
### Changed
- Default duration for tweens and `node.to()` methods is now 300ms
- Typescript fixes
- Automatic validations for many attributes
## 2.2.1 - 2018-08-10
### Added
- New properties for `Konva.Transformer`: `borderStroke`, `borderStrokeWidth`, `borderDash`, `anchorStroke`, `anchorStrokeWidth`, `anchorSize`.
### Changed
- Some properties of `Konva.Transformer` are renamed. `lineEnabled` -> `borderEnabled`. `rotateHandlerOffset` -> `rotateAnchorOffset`, `enabledHandlers` -> `enabledAnchors`.
## 2.1.8 - 2018-08-01
### Fixed
- Some `Konva.Transformer` fixes
- Typescript fixes
- `stage.toDataURL()` fixes when it has hidden layers
- `shape.toDataURL()` automatically adjust position and size of resulted image
## 2.1.7 - 2018-07-03
### Fixed
- `toObject` fixes
## 2.1.7 - 2018-07-03
### Fixed
- Some drag&drop fixes
## 2.1.6 - 2018-06-16
### Fixed
- Removed wrong dep
- Typescript fixes
## 2.1.5 - 2018-06-15
### Fixed
- Typescript fixes
- add shape as second argument for `sceneFunc` and `hitFunc`
## 2.1.4 - 2018-06-15
### Fixed
- Fixed `Konva.Text` justify drawing for a text with decoration
- Added methods `data()`,`setData()` and `getData()` methods to `Konva.TextPath`
- Correct cache reset for `Konva.Transformer`
## 2.1.3 - 2018-05-17
### Fixed
- `Konva.Transformer` automatically track shape changes
- `Konva.Transformer` works with shapes with offset too
## 2.1.2 - 2018-05-16
### Fixed
- Cursor fixes for `Konva.Transformer`
- Fixed lineHeight behavior for `Konva.Text`
- Some performance optimizations for `Konva.Text`
- Better wrap algorithm for `Konva.Text`
- fixed `Konva.Arrow` with tension != 0
- Some fixes for `Konva.Transformer`
## 2.0.3 - 2018-04-21
### Added
- Typescript defs for `Konva.Transformer`
- Typescript defs for `globalCompositeOperation`
## Changes
- Fixed flow for `contextmenu` event. Now it will be triggered on shapes too
- `find()` method for Containers can use a function as a parameter
### Fixed
- some bugs fixes for `group.getClientRect()`
- `Konva.Arrow` will not draw dash for pointers
- setAttr will trigger change event if new value is the same Object
- better behavior of `dblclick` event when you click fast on different shapes
- `stage.toDataURL` will use `pixelRatio = 1` by default.
## 2.0.2 - 2018-03-15
### Fixed
- Even more bugs fixes for `Konva.Transformer`
## 2.0.1 - 2018-03-15
### Fixed
- Several bugs fixes for `Konva.Transformer`
## 2.0.0 - 2018-03-15
### Added
- new `Konva.Transformer`. It is a special group that allow simple resizing and rotation of a shape.
- Add ability to remove event by callback `node.off('event', callback)`.
- new `Konva.Filters.Contrast`.
- new `Konva.Util.haveIntersection()` to detect simple collusion
- add `Konva.Text.ellipsis` to add '…' to text string if width is fixed and wrap is set to 'none'
- add gradients for strokes
## Changed
- stage events are slightly changed. `mousedown`, `click`, `mouseup`, `dblclick`, `touchstart`, `touchend`, `tap`, `dbltap` will be triggered when clicked on empty areas too
### Fixed
- Some typescript fixes
- Pixelate filter fixes
- Fixes for path data parsing
- Fixed shadow size calculation
## Removed
- Some deprecated methods are removed. If previous version was working without deprecation warnings for you, this one will work fine too.
## 1.7.6 - 2017-11-01
### Fixed
- Some typescript fixes
## 1.7.4 - 2017-10-30
### Fixed
- `isBrowser` detection for electron
## 1.7.3 - 2017-10-19
### Changed
- Changing size of a stage will redraw it in synchronous way
### Fixed
- Some fixes special for nodejs
## 1.7.2 - 2017-10-11
### Fixed
- Fixed `Konva.document is undefined`
## 1.7.1 - 2017-10-11
### Changed
- Konva for browser env and Konva for nodejs env are separate packages now. You can use `konva-node` for NodeJS env.
## 1.7.0 - 2017-10-08
### Fixed
- Several typescript fixes
### Changed
- Default value for `dragDistance` is changed to 3px.
- Fix rare error throw on drag
- Caching with height = 0 or width = 0 with throw async error. Caching will be ignored.
## 1.6.8 - 2017-08-19
### Changed
- The `node.getClientRect()` calculation is changed a bit. It is more powerfull and correct. Also it takes parent transform into account. See docs.
- Upgrade nodejs deps
## 1.6.7 - 2017-07-28
### Fixed
- Fix bug with double trigger wheel in Firefox
- Fix `node.getClientRect()` calculation in a case of Group + invisible child
- Fix dblclick issue https://github.com/konvajs/konva/issues/252
## 1.6.3 - 2017-05-24
### Fixed
- Fixed bug with pointer detection. css 3d transformed stage will not work now.
## 1.6.2 - 2017-05-08
### Fixed
- Fixed bug with automatic shadow for negative scale values
## 1.6.1 - 2017-04-25
### Fixed
- Fix pointer position detection
### Changed
- moved `globalCompositeOperation` property to `Konva.Node`
## 1.6.0 - 2017-04-21
### Added
- support of globalCompositeOperation for `Konva.Shape`
### Fixed
- getAllIntersections now works ok for Text shapes (https://github.com/konvajs/konva/issues/224)
### Changed
- Konva a bit changed a way to detect pointer position. Now it should be OK to apply css transform on Konva container. https://github.com/konvajs/konva/pull/215
## 1.5.0 - 2017-03-20
### Added
- support for `lineDashOffset` property for `Konva.Shape`.
## 1.4.0 - 2017-02-07
## Added
- `textDecoration` of `Konva.Text` now supports `line-through`
## 1.3.0 - 2017-01-10
## Added
- new align value for `Konva.Text` and `Konva.TextPath`: `justify`
- new property for `Konva.Text` and `Konva.TextPath`: `textDecoration`. Right now it sports only '' (no decoration) and 'underline' values.
- new property for `Konva.Text`: `letterSpacing`
- new event `contentContextmenu` for `Konva.Stage`
- `align` support for `Konva.TextPath`
- new method `toCanvas()` for converting a node into canvas element
### Changed
- changing a size of `Konva.Stage` will update it in async way (via `batchDraw`).
- `shadowOffset` respect pixel ratio now
### Fixed
- Fixed bug when `Konva.Tag` width was not changing its width dynamically
- Fixed "calling remove() for dragging shape will throw an error"
- Fixed wrong opacity level for cached group with opacity
- More consistent shadows on HDPI screens
- Fixed memory leak for nodes with several names
## 1.2.2 - 2016-09-15
### Fixed
- refresh stage hit and its `dragend`
- `getClientRect` calculations
## 1.2.0 - 2016-09-15
## Added
- new properties for `Konva.TextPath`: `letterSpacing` and `textBaseline`.
## 1.1.4 - 2016-09-13
### Fixed
- Prevent throwing an error when text property of `Konva.Text` = undefined or null
## 1.1.3 - 2016-09-12
### Changed
- Better hit function for `TextPath`.
- Validation of `Shape` filters.
## 1.1.2 - 2016-09-10
### Fixed
- Fixed "Dragging Group on mobile view throws "missing preventDefault" error" #169
## 1.1.1 - 2016-08-30
### Fixed
- Fixed #166 bug of drag&drop
## 1.1.0 - 2016-08-21
## Added
- new property of `Konva.Shape` - `preventDefault`.
## 1.0.3 - 2016-08-14
### Fixed
- Fixed some typescript definitions
## 1.0.2 - 2016-07-08
## Changed
- `Konva.Text` will interpret undefined `width` and `height` as `AUTO`
## 1.0.1 - 2016-07-05
### Changed
- you can now unset property by `node.x(undefined)` or `node.setAttr('x', null)`
### Fixed
- Bug fix for case when `touchend` event throws error
## 1.0.0 - 2016-07-05
### Fixed
- Bug fix for case when `touchend` event throws error
## 0.15.0 - 2016-06-18
## Added
- Custom clip function
## 0.14.0 - 2016-06-17
### Fixed
- fixes in typescript definitions
- fixes for bug with `mouseenter` event on deep nesting case
## 0.13.9 - 2016-05-14
### Changed
- typescript definition in npm package
- node@5.10.1, canvas@1.3.14, jsdom@8.5.0 support
- `Konva.Path` will be filled when it is not closed
- `Animation.start()` will not not immediate sync draw. This should improve performance a little.
- Warning when node for `Tween` is not in layer yet.
- `removeChildren()` remove only first level children. So it will not remove grandchildren.
## 0.12.4 - 2016-04-19
### Changed
- `batchDraw` will do not immediate `draw()`
### Fixed
- fix incorrect shadow offset on rotation
## 0.12.3 - 2016-04-07
### Fixed
- `batchDraw` function works less time now
- lighter npm package
## 0.12.2 - 2016-03-31
### Fixed
- repair `cancelBubble` event property behaviour
- fix wrong `Path` `getClientRect()` calculation
- better HDPI support
- better typescript definitions
- node 0.12 support
### Changed
- more universal stage container selector
- `mousewheel` event changed to `wheel`
## 0.11.1 - 2016-01-16
### Fixed
- correct `Konva.Arrow` drawing. Now it works better.
- Better support for dragging when mouse out of stage
- Better corner radius for `Label` shape
- `contentTap` event for stage
### Added
- event delegation. You can use it in this way: `layer.on('click', 'Circle', handler);`
- new `node.findAncestors(selector)` and `node.findAncestor(selector)` functions
- optional selector parameter for `stage.getIntersection` and `layer.getIntersection`
- show warning message if several instances of Konva are added to page.
### Changed
- `moveTo` and some other methods return `this`
- `getAbsolutePosition` support optional relative parent argument (useful to find absolute position inside of some of parent nodes)
- `change` event will be not fired if changed value is the same as old value
## 0.10.0 - 2015-10-27
### Added
- RGBA filter. Thanks to [@codefo](https://github.com/codefo)
- `stroke` and `fill` support for `Konva.Sprite`
### Fixed
- Correct calculation in `getClientRect` method of `Konva.Line` and `Konva.Container`.
- Correct `toObject()` behaviour for node with attrs with extended native prototypes
- Fixed bug for caching where buffer canvas is required
### Changed
- Dragging works much better. If your pointer is out of stage content dragging will still continue.
- `Konva.Node.create` now works with objects.
- `Konva.Tween` now supports tweening points to state with different length
## 0.9.5 - 2015-05-28
### Fixed
- `to` will not throw error if no `onFinish` callback
- HDPI support for desktop
- Fix bug when filters are not correct for HDPI
- Fix bug when hit area is not correct for HDPI
- Fix bug for incorrect `getClientRect` calculation
- Repair fill gradient for text
### Changed
- context wrapper is more capable with native context.
So you can use `context.fillStyle` property in your `sceneFunc` without accessing native context.
- `toDataURL` now handles pixelRatio. you can pass `config.pixelRatio` argument
- Correct `clone()` for custom nodes
- `FastLayer` can now have transforms
- `stage.toDataURL()` method now works synchronously. So `callback` argument is not required.
- `container.find(selector)` method now has a validation step. So if you forgot to add `#` or `.` you will see a warning message in the console.
### Added
- new `Konva.Image.fromURL` method
### Deprecated
- `fillRed`, `fillGreen`, `fillBlue`, `fillAlpha` are deprecated. Use `fill` instead.
- `strokeRed`, `strokeGreen`, `strokeBlue`, `strokeAlpha` are deprecated. Use `stroke` instead.
- `shadowRed`, `shadowGreen`, `shadowBlue`, `shadowAlpha` are deprecated. Use `shadow` instead.
- `dashArray` is deprecated. Use `dash` instead.
- `drawFunc` is deprecated. Use `sceneFunc` instead.
- `drawHitFunc` is deprecated. Use `hitFunc` instead.
- `rotateDeg` is deprecated. Use `rotate` instead.
## 0.9.0 - 2015-02-27
### Fixed
- cache algorithm has A LOT OF updates.
### Changed
- `scale` now affects `shadowOffset`
- performance optimization (remove some unnecessary draws)
- more expected drawing when shape has opacity, stroke and shadow
- HDPI for caching.
- Cache should work much better. Now you don't need to pass bounding box {x,y,width,height} to `cache` method for all buildin Konva shapes. (only for your custom `Konva.Shape` instance).
- `Tween` now supports color properties (`fill`, `stroke`, `shadowColor`)
### Added
- new methods for working with node's name: `addName`, `removeName`, `hasName`.
- new `perfectDrawEnabled` property for shape. See [http://konvajs.org/docs/performance/Disable_Perfect_Draw.html](http://konvajs.org/docs/performance/Disable_Perfect_Draw.html)
- new `shadowForStrokeEnabled` property for shape. See [http://konvajs.org/docs/performance/All_Performance_Tips.html](http://konvajs.org/docs/performance/All_Performance_Tips.html)
- new `getClientRect` method.
- new `to` method for every node for shorter tweening
## 0.8.0 - 2015-02-04
- Bug Fixes
- browser crashing on pointer events fixed
- optimized `getIntersection` function
- Enhancements
- `container.findOne()` method
- new `strokeHitEnabled` property. Useful for performance optimizations
- typescript definitions. see `/resources/konva.d.ts`
## Rebranding release 2015-01-28
Differences from last official `KineticJS` release
- Bug Fixes
- `strokeScaleEnabled = false` is disabled for text as I can not find a way to implement this
- `strokeScaleEnabled = false` for Line now creates a correct hit graph
- working "this-example" as name for nodes
- Konva.Text() with no config will not throw exception
- Konva.Line() with no config will not throw exception
- Correct stage resizing with `FastLayer`
- `batchDraw` method for `FastLayer`
- Correct mouseover/mouseout/mouseenter/mouseleave events for groups
- cache node before adding to layer
- `intersects` function now works for shapes with shadow
- Enhancements
- `cornerRadius` of Rect is limited by `width/2` and `height/2`
- `black` is default fill for text
- true class extending. Now `rect instanceOf Konva.Shape` will return true
- while dragging you can redraw layer that is not under drag. hit graph will be updated in this case
- now you can move object that is dragging into another layer.
- new `frameOffsets` attribute for `Konva.Sprite`
- much better dragging performance
- `browserify` support
- applying opacity to cached node
- remove all events with `node.off()`
- mouse dragging only with left button
- opacity now affects cached shapes
- Label corner radius
- smart changing `width`, `height`, `radius` attrs for circle, start, ellipse, ring.
- `mousewheel` support. Thanks [@vmichnowicz](https://github.com/vmichnowicz)
- new Arrow plugin
- multiple names: `node.name('foo bar'); container.find('.foo');` (thanks [@mattslocum](https://github.com/mattslocum))
- `Container.findOne()`
================================================
FILE: LICENSE
================================================
MIT License
Original work Copyright (C) 2011 - 2013 by Eric Rowell (KineticJS)
Modified work Copyright (C) 2014 - present by Anton Lavrenov (Konva)
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
================================================
Konva
[](https://opencollective.com/konva)
[](http://badge.fury.io/js/konva)
[](https://github.com/konvajs/konva/actions/workflows/test-browser.ym)
[](https://github.com/konvajs/konva/actions/workflows/test-node.ym)[](https://cdnjs.com/libraries/konva)
Konva is an HTML5 Canvas JavaScript framework that enables high performance animations, transitions, node nesting, layering, filtering, caching, event handling for desktop and mobile applications, and much more.
You can draw things onto the stage, add event listeners to them, move them, scale them, and rotate them independently from other shapes to support high performance animations, even if your application uses thousands of shapes. Served hot with a side of awesomeness.
This repository began as a GitHub fork of [ericdrowell/KineticJS](https://github.com/ericdrowell/KineticJS).
- **Visit:** The [Home Page](http://konvajs.org/) and follow on [Twitter](https://twitter.com/lavrton)
- **Discover:** [Tutorials](http://konvajs.org/docs/index.html), [API Documentation](http://konvajs.org/api/Konva.html)
- **Help:** [StackOverflow](http://stackoverflow.com/questions/tagged/konvajs), [Discord Chat](https://discord.gg/8FqZwVT)
# Quick Look
```html
```
# Browsers support
Konva works in all modern mobile and desktop browsers. A browser need to be capable to run javascript code from ES2015 spec. For older browsers you may need polyfills for missing functions.
At the current moment `Konva` doesn't work in IE11 directly. To make it work you just need to provide some polyfills such as `Array.prototype.find`, `String.prototype.trimLeft`, `String.prototype.trimRight`, `Array.from`.
# Debugging
The Chrome inspector simply shows the canvas element. To see the Konva objects and their details, install the konva-dev extension at https://github.com/konvajs/konva-devtool.
# Loading and installing Konva
Konva supports UMD loading. So you can use all possible variants to load the framework into your project:
### Load Konva via classical `
```
### Install with npm:
```bash
npm install konva --save
```
```javascript
// The modern way (e.g. an ES6-style import for webpack, parcel)
import Konva from 'konva';
```
#### Typescript usage
Add DOM definitions into your `tsconfig.json`:
```
{
"compilerOptions": {
"lib": [
"es6",
"dom"
]
}
}
```
### 3 Minimal bundle
```javascript
import Konva from 'konva/lib/Core';
// Now you have a Konva object with Stage, Layer, FastLayer, Group, Shape and some additional utils function.
// Also core currently already have support for drag&drop and animations.
// BUT there are no shapes (rect, circle, etc), no filters.
// but you can simply add anything you need:
import { Rect } from 'konva/lib/shapes/Rect';
// importing a shape will automatically inject it into Konva object
var rect1 = new Rect();
// or:
var shape = new Konva.Rect();
// for filters you can use this:
import { Blur } from 'konva/lib/filters/Blur';
```
### 4 NodeJS env
In order to run `konva` in nodejs environment you also need to install `canvas` or `skia-canvas` package manually for rendering backend.
```bash
# node-canvas backend
npm install konva canvas
# skia-canvas backend
npm install konva skia-canvas
```
Then you can use the same Konva API and all Konva demos will work just fine. You just don't need to use `container` attribute in your stage.
```js
import Konva from 'konva';
import 'konva/canvas-backend'; // or import 'konva/skia-backend';
const stage = new Konva.Stage({
width: 500,
height: 500,
});
// then all regular Konva code will work
```
# Backers


- [myposter GmbH](https://www.myposter.de/)
- [queue.gg](https://queue.gg/)
# Change log
See [CHANGELOG.md](https://github.com/konvajs/konva/blob/master/CHANGELOG.md).
## Building the Konva Framework
To make a full build run `npm run build`. The command will compile all typescript files, combine then into one bundle and run minifier.
## Testing
Konva uses Mocha for testing.
- If you need run test only one time run `npm run test`.
- While developing it is easy to use `npm start`. Just run it and go to [http://localhost:1234/unit-tests.html](http://localhost:1234/unit-tests.html). The watcher will rebuild the bundle on any change.
Konva is covered with hundreds of tests and well over a thousand assertions.
Konva uses TDD (test driven development) which means that every new feature or bug fix is accompanied with at least one new test.
## Generate documentation
Run `npx gulp api` which will build the documentation files and place them in the `api` folder.
# Pull Requests
I'd be happy to review any pull requests that may better the Konva project,
in particular if you have a bug fix, enhancement, or a new shape (see `src/shapes` for examples). Before doing so, please first make sure that all of the tests pass (`npm run test`).
## Contributors
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/konva/contribute)]
#### Individuals
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/konva/contribute)]
================================================
FILE: gulpfile.mjs
================================================
import gulp from 'gulp';
import rename from 'gulp-rename';
import uglify from 'gulp-uglify-es';
import replace from 'gulp-replace';
import jsdoc from 'gulp-jsdoc3';
import connect from 'gulp-connect';
import gutil from 'gulp-util';
import fs from 'fs';
var NodeParams = fs
.readFileSync('./resources/doc-includes/NodeParams.txt')
.toString();
var ContainerParams = fs
.readFileSync('./resources/doc-includes/ContainerParams.txt')
.toString();
var ShapeParams = fs
.readFileSync('./resources/doc-includes/ShapeParams.txt')
.toString();
const conf = JSON.parse(fs.readFileSync('./package.json'));
function build() {
return gulp
.src(['./konva.js'])
.pipe(replace('@@shapeParams', ShapeParams))
.pipe(replace('@@nodeParams', NodeParams))
.pipe(replace('@@containerParams', ContainerParams))
.pipe(replace('@@version', conf.version))
.pipe(replace('@@date', new Date().toDateString()));
}
gulp.task('update-version-lib', function () {
return gulp
.src(['./lib/Global.js'])
.pipe(replace('@@version', conf.version))
.pipe(rename('Global.js'))
.pipe(gulp.dest('./lib'));
});
gulp.task('update-version-cmj', function () {
return gulp
.src(['./cmj/Global.js'])
.pipe(replace('@@version', conf.version))
.pipe(rename('Global.js'))
.pipe(gulp.dest('./cmj'));
});
gulp.task('update-version-es-to-cmj-index', function () {
return gulp
.src(['./lib/index.js'])
.pipe(
replace(`import { Konva } from './_F`, `import { Konva } from '../cmj/_F`)
)
.pipe(rename('index.js'))
.pipe(gulp.dest('./lib'));
});
gulp.task('update-version-es-to-cmj-node', function () {
return gulp
.src(['./lib/index-node.js'])
.pipe(
replace(`import { Konva } from './_F`, `import { Konva } from '../cmj/_F`)
)
.pipe(rename('index-node.js'))
.pipe(gulp.dest('./lib'));
});
// create usual build konva.js and konva.min.js
gulp.task('pre-build', function () {
return build()
.pipe(rename('konva.js'))
.pipe(gulp.dest('./'))
.pipe(
uglify.default({ output: { comments: /^!|@preserve|@license|@cc_on/i } })
)
.on('error', function (err) {
gutil.log(gutil.colors.red('[Error]'), err.toString());
})
.pipe(rename('konva.min.js'))
.pipe(gulp.dest('./'));
});
gulp.task(
'build',
gulp.parallel([
'update-version-lib',
// 'update-version-cmj',
// 'update-version-es-to-cmj-index',
// 'update-version-es-to-cmj-node',
'pre-build',
])
);
// local server for better development
gulp.task('server', function () {
connect.server();
});
// // generate documentation
gulp.task('api', function () {
return gulp.src('./konva.js').pipe(
jsdoc({
opts: {
destination: './api',
},
})
);
});
gulp.task('default', gulp.parallel(['server']));
================================================
FILE: package.json
================================================
{
"name": "konva",
"version": "10.2.3",
"description": "HTML5 2d canvas library.",
"author": "Anton Lavrenov",
"type": "module",
"files": [
"README.md",
"konva.js",
"konva.min.js",
"lib"
],
"exports": {
".": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
},
"./lib/*": {
"types": "./lib/*.d.ts",
"default": "./lib/*.js"
},
"./lib/*.js": {
"types": "./lib/*.d.ts",
"default": "./lib/*.js"
},
"./canvas-backend": {
"types": "./lib/canvas-backend.d.ts",
"default": "./lib/canvas-backend.js"
},
"./skia-backend": {
"types": "./lib/skia-backend.d.ts",
"default": "./lib/skia-backend.js"
}
},
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"scripts": {
"start": "npm run test:watch",
"compile": "npm run clean && npm run tsc && npm run rollup",
"build": "npm run compile && gulp build",
"fmt": "prettier --write .",
"fmt:check": "prettier --check .",
"test:import": "npm run build && node ./test/import-test.cjs && node ./test/import-test.mjs",
"test": "npm run test:browser && npm run test:node && npm run test:import",
"test:build": "PARCEL_WORKER_BACKEND=process parcel build ./test/unit-tests.html --dist-dir ./test-build --target none --public-url ./ --no-source-maps",
"test:browser": "npm run test:build && mocha-headless-chrome -f ./test-build/unit-tests.html -a disable-web-security -a no-sandbox -a disable-setuid-sandbox",
"test:watch": "rimraf ./.parcel-cache && PARCEL_WORKERS=0 parcel serve ./test/unit-tests.html ./test/manual-tests.html ./test/sandbox.html ./test/text-paths.html ./test/bunnies.html",
"test:node:canvas": "mocha -r tsx -r ./test/node-canvas-global-setup.mjs --extension ts --recursive './test/unit/' --exit",
"test:node:skia": "mocha -r tsx -r ./test/node-skia-global-setup.mjs --extension ts --recursive './test/unit/' --exit",
"test:node": "npm run test:node:canvas && npm run test:node:skia",
"tsc": "tsc --removeComments",
"rollup": "rollup -c",
"clean": "rimraf ./lib && rimraf ./types && rimraf ./cmj && rimraf ./test-build",
"watch": "rollup -c -w",
"size": "size-limit"
},
"targets": {
"none": {}
},
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/lavrton"
},
{
"type": "opencollective",
"url": "https://opencollective.com/konva"
},
{
"type": "github",
"url": "https://github.com/sponsors/lavrton"
}
],
"size-limit": [
{
"limit": "45 KB",
"path": "./lib/index.js"
},
{
"limit": "26 KB",
"path": "./lib/Core.js"
},
{
"path": "./konva.min.js"
}
],
"devDependencies": {
"@parcel/transformer-image": "2.15.4",
"@size-limit/preset-big-lib": "^11.2.0",
"@types/mocha": "^10.0.10",
"canvas": "^3.2.1",
"chai": "6.2.2",
"filehound": "^1.17.6",
"gulp": "^5.0.1",
"gulp-concat": "^2.6.1",
"gulp-connect": "^5.7.0",
"gulp-exec": "^5.0.0",
"gulp-jsdoc3": "^3.0.0",
"gulp-rename": "^2.1.0",
"gulp-replace": "^1.1.4",
"gulp-typescript": "^5.0.1",
"gulp-uglify": "^3.0.2",
"gulp-uglify-es": "^3.0.0",
"gulp-util": "^3.0.8",
"mocha": "^10",
"mocha-headless-chrome": "^4.0.0",
"parcel": "2.15.4",
"prettier": "^3.7.4",
"process": "^0.11.10",
"rimraf": "^6.1.2",
"rollup": "^4.55.1",
"rollup-plugin-typescript2": "^0.36.0",
"size-limit": "^11.2.0",
"skia-canvas": "^3.0.4",
"ts-mocha": "^11.1.0",
"ts-node": "^10.9.2",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
},
"keywords": [
"canvas",
"animations",
"graphic",
"html5"
],
"prettier": {
"singleQuote": true,
"trailingComma": "es5"
},
"bugs": {
"url": "https://github.com/konvajs/konva/issues"
},
"homepage": "http://konvajs.org/",
"readmeFilename": "README.md",
"repository": {
"type": "git",
"url": "git://github.com/konvajs/konva.git"
},
"license": "MIT"
}
================================================
FILE: release.sh
================================================
#!/usr/bin/env bash
set -e
old_version="$(git describe --abbrev=0 --tags)"
new_version=$1
old_cdn_min="https://unpkg.com/konva@${old_version}/konva.min.js"
new_cdn_min="https://unpkg.com/konva@${new_version}/konva.min.js"
# make sure new version parameter is passed
if [ -z "$1" ]
then
echo "ERROR - pass new version. usage release.sh 0.11.0"
exit 2
fi
# make sure changle log updated
while true; do
read -p "Did you update CHANGELOG.md? " yn
case $yn in
[Yy]* ) break;;
[Nn]* ) exit;;
* ) echo "Please answer yes or no.";;
esac
done
echo "Old version: ${old_version}"
echo "New version: ${new_version}"
echo "Pulling"
git pull >/dev/null
echo "build and test"
npm run build >/dev/null
# npm run test
echo "commit change log updates"
git commit -am "update CHANGELOG with new version" --allow-empty >/dev/null
echo "npm version $1 --no-git-tag-version"
npm version $1 --no-git-tag-version --allow-same-version >/dev/null
echo "build for $1"
npm run build >/dev/null
git commit -am "build for $1" --allow-empty >/dev/null
echo "update CDN link in README"
perl -i -pe "s|${old_cdn_min}|${new_cdn_min}|g" ./README.md >/dev/null
git commit -am "update cdn link" --allow-empty >/dev/null
echo "create new git tag"
git tag $1 >/dev/null
cd ../konva
git push >/dev/null
git push --tags >/dev/null
npm publish
echo "DONE!"
================================================
FILE: resources/doc-includes/ContainerParams.txt
================================================
* @param {Object} [config.clip] set clip
* @param {Number} [config.clipX] set clip x
* @param {Number} [config.clipY] set clip y
* @param {Number} [config.clipWidth] set clip width
* @param {Number} [config.clipHeight] set clip height
* @param {Function} [config.clipFunc] set clip func
================================================
FILE: resources/doc-includes/NodeParams.txt
================================================
@param {Number} [config.x]
* @param {Number} [config.y]
* @param {Number} [config.width]
* @param {Number} [config.height]
* @param {Boolean} [config.visible]
* @param {Boolean} [config.listening] whether or not the node is listening for events
* @param {String} [config.id] unique id
* @param {String} [config.name] non-unique name
* @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1
* @param {Object} [config.scale] set scale
* @param {Number} [config.scaleX] set scale x
* @param {Number} [config.scaleY] set scale y
* @param {Number} [config.rotation] rotation in degrees
* @param {Object} [config.offset] offset from center point and rotation point
* @param {Number} [config.offsetX] set offset x
* @param {Number} [config.offsetY] set offset y
* @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop
* the entire stage by dragging any portion of the stage
* @param {Number} [config.dragDistance]
* @param {Function} [config.dragBoundFunc]
================================================
FILE: resources/doc-includes/ShapeParams.txt
================================================
@param {String} [config.fill] fill color
* @param {Image} [config.fillPatternImage] fill pattern image
* @param {Number} [config.fillPatternX]
* @param {Number} [config.fillPatternY]
* @param {Object} [config.fillPatternOffset] object with x and y component
* @param {Number} [config.fillPatternOffsetX]
* @param {Number} [config.fillPatternOffsetY]
* @param {Object} [config.fillPatternScale] object with x and y component
* @param {Number} [config.fillPatternScaleX]
* @param {Number} [config.fillPatternScaleY]
* @param {Number} [config.fillPatternRotation]
* @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat"
* @param {Object} [config.fillLinearGradientStartPoint] object with x and y component
* @param {Number} [config.fillLinearGradientStartPointX]
* @param {Number} [config.fillLinearGradientStartPointY]
* @param {Object} [config.fillLinearGradientEndPoint] object with x and y component
* @param {Number} [config.fillLinearGradientEndPointX]
* @param {Number} [config.fillLinearGradientEndPointY]
* @param {Array} [config.fillLinearGradientColorStops] array of color stops
* @param {Object} [config.fillRadialGradientStartPoint] object with x and y component
* @param {Number} [config.fillRadialGradientStartPointX]
* @param {Number} [config.fillRadialGradientStartPointY]
* @param {Object} [config.fillRadialGradientEndPoint] object with x and y component
* @param {Number} [config.fillRadialGradientEndPointX]
* @param {Number} [config.fillRadialGradientEndPointY]
* @param {Number} [config.fillRadialGradientStartRadius]
* @param {Number} [config.fillRadialGradientEndRadius]
* @param {Array} [config.fillRadialGradientColorStops] array of color stops
* @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true
* @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration
* @param {String} [config.stroke] stroke color
* @param {Number} [config.strokeWidth] stroke width
* @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false.
* @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth
* @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true
* @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true
* @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true
* @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true
* @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true
* @param {String} [config.lineJoin] can be miter, round, or bevel. The default
* is miter
* @param {String} [config.lineCap] can be butt, round, or square. The default
* is butt
* @param {String} [config.shadowColor]
* @param {Number} [config.shadowBlur]
* @param {Object} [config.shadowOffset] object with x and y component
* @param {Number} [config.shadowOffsetX]
* @param {Number} [config.shadowOffsetY]
* @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number
* between 0 and 1
* @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true
* @param {Array} [config.dash]
* @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true
================================================
FILE: resources/jsdoc.conf.json
================================================
{
"path": "ink-docstrap",
"tags": {
"allowUnknownTags": true
},
"plugins": ["plugins/markdown"],
"templates": {
"cleverLinks": false,
"monospaceLinks": false,
"dateFormat": "ddd MMM Do YYYY",
"outputSourceFiles": true,
"outputSourcePath": true,
"systemName": "Konva",
"footer": "",
"copyright": "Konva Copyright © 2015 The contributors to the Konva project.",
"navType": "vertical",
"theme": "cosmo",
"linenums": true,
"collapseSymbols": false,
"inverseNav": true,
"highlightTutorialCode": true
},
"markdown": {
"parser": "gfm",
"hardwrap": true
}
}
================================================
FILE: rollup.config.mjs
================================================
// import resolve from 'rollup-plugin-node-resolve';
import typescript from 'rollup-plugin-typescript2';
export default {
input: `src/index.ts`,
output: [
{
file: 'konva.js',
name: 'Konva',
format: 'umd',
sourcemap: false,
freeze: false,
},
],
external: [],
watch: {
include: 'src/**',
},
plugins: [
// Compile TypeScript files
typescript({
useTsconfigDeclarationDir: true,
abortOnError: false,
removeComments: false,
tsconfigOverride: {
compilerOptions: {
module: 'ES2020',
},
},
}),
],
};
================================================
FILE: src/Animation.ts
================================================
import { glob } from './Global.ts';
import type { Layer } from './Layer.ts';
import type { IFrame, AnimationFn } from './types.ts';
import { Util } from './Util.ts';
const now = (function (): () => number {
if (glob.performance && glob.performance.now) {
return function () {
return glob.performance.now();
};
}
return function () {
return new Date().getTime();
};
})();
/**
* Animation constructor.
* @constructor
* @memberof Konva
* @param {AnimationFn} func function executed on each animation frame. The function is passed a frame object, which contains
* timeDiff, lastTime, time, and frameRate properties. The timeDiff property is the number of milliseconds that have passed
* since the last animation frame. The time property is the time in milliseconds that elapsed from the moment the animation started
* to the current animation frame. The lastTime property is a `time` value from the previous frame. The frameRate property is the current frame rate in frames / second.
* Return false from function, if you don't need to redraw layer/layers on some frames.
* @param {Konva.Layer|Array} [layers] layer(s) to be redrawn on each animation frame. Can be a layer, an array of layers, or null.
* Not specifying a node will result in no redraw.
* @example
* // move a node to the right at 50 pixels / second
* var velocity = 50;
*
* var anim = new Konva.Animation(function(frame) {
* var dist = velocity * (frame.timeDiff / 1000);
* node.move({x: dist, y: 0});
* }, layer);
*
* anim.start();
*/
export class Animation {
func: AnimationFn;
id = Animation.animIdCounter++;
layers: Layer[];
frame: IFrame = {
time: 0,
timeDiff: 0,
lastTime: now(),
frameRate: 0,
};
constructor(func: AnimationFn, layers?) {
this.func = func;
this.setLayers(layers);
}
/**
* set layers to be redrawn on each animation frame
* @method
* @name Konva.Animation#setLayers
* @param {Konva.Layer|Array} [layers] layer(s) to be redrawn. Can be a layer, an array of layers, or null. Not specifying a node will result in no redraw.
* @return {Konva.Animation} this
*/
setLayers(layers: null | Layer | Layer[]) {
let lays: Layer[] = [];
// if passing in no layers
if (layers) {
lays = Array.isArray(layers) ? layers : [layers];
}
this.layers = lays;
return this;
}
/**
* get layers
* @method
* @name Konva.Animation#getLayers
* @return {Array} Array of Konva.Layer
*/
getLayers() {
return this.layers;
}
/**
* add layer. Returns true if the layer was added, and false if it was not
* @method
* @name Konva.Animation#addLayer
* @param {Konva.Layer} layer to add
* @return {Bool} true if layer is added to animation, otherwise false
*/
addLayer(layer: Layer) {
const layers = this.layers;
const len = layers.length;
// don't add the layer if it already exists
for (let n = 0; n < len; n++) {
if (layers[n]._id === layer._id) {
return false;
}
}
this.layers.push(layer);
return true;
}
/**
* determine if animation is running or not. returns true or false
* @method
* @name Konva.Animation#isRunning
* @return {Bool} is animation running?
*/
isRunning() {
const a = Animation;
const animations = a.animations;
const len = animations.length;
for (let n = 0; n < len; n++) {
if (animations[n].id === this.id) {
return true;
}
}
return false;
}
/**
* start animation
* @method
* @name Konva.Animation#start
* @return {Konva.Animation} this
*/
start() {
this.stop();
this.frame.timeDiff = 0;
this.frame.lastTime = now();
Animation._addAnimation(this);
return this;
}
/**
* stop animation
* @method
* @name Konva.Animation#stop
* @return {Konva.Animation} this
*/
stop() {
Animation._removeAnimation(this);
return this;
}
_updateFrameObject(time: number) {
this.frame.timeDiff = time - this.frame.lastTime;
this.frame.lastTime = time;
this.frame.time += this.frame.timeDiff;
this.frame.frameRate = 1000 / this.frame.timeDiff;
}
static animations: Array = [];
static animIdCounter = 0;
static animRunning = false;
static _addAnimation(anim) {
this.animations.push(anim);
this._handleAnimation();
}
static _removeAnimation(anim) {
const id = anim.id;
const animations = this.animations;
const len = animations.length;
for (let n = 0; n < len; n++) {
if (animations[n].id === id) {
this.animations.splice(n, 1);
break;
}
}
}
static _runFrames() {
const layerHash = {};
const animations = this.animations;
/*
* loop through all animations and execute animation
* function. if the animation object has specified node,
* we can add the node to the nodes hash to eliminate
* drawing the same node multiple times. The node property
* can be the stage itself or a layer
*/
/*
* WARNING: don't cache animations.length because it could change while
* the for loop is running, causing a JS error
*/
for (let n = 0; n < animations.length; n++) {
const anim = animations[n];
const layers = anim.layers;
const func = anim.func;
anim._updateFrameObject(now());
const layersLen = layers.length;
// if animation object has a function, execute it
let needRedraw;
if (func) {
// allow anim bypassing drawing
needRedraw = func.call(anim, anim.frame) !== false;
} else {
needRedraw = true;
}
if (!needRedraw) {
continue;
}
for (let i = 0; i < layersLen; i++) {
const layer = layers[i];
if (layer._id !== undefined) {
layerHash[layer._id] = layer;
}
}
}
for (const key in layerHash) {
if (!layerHash.hasOwnProperty(key)) {
continue;
}
layerHash[key].batchDraw();
}
}
static _animationLoop() {
const Anim = Animation;
if (Anim.animations.length) {
Anim._runFrames();
Util.requestAnimFrame(Anim._animationLoop);
} else {
Anim.animRunning = false;
}
}
static _handleAnimation() {
if (!this.animRunning) {
this.animRunning = true;
Util.requestAnimFrame(this._animationLoop);
}
}
}
================================================
FILE: src/BezierFunctions.ts
================================================
// Credits: rveciana/svg-path-properties
// Legendre-Gauss abscissae (xi values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x))
export const tValues = [
[],
[],
[
-0.5773502691896257645091487805019574556476,
0.5773502691896257645091487805019574556476,
],
[
0, -0.7745966692414833770358530799564799221665,
0.7745966692414833770358530799564799221665,
],
[
-0.3399810435848562648026657591032446872005,
0.3399810435848562648026657591032446872005,
-0.8611363115940525752239464888928095050957,
0.8611363115940525752239464888928095050957,
],
[
0, -0.5384693101056830910363144207002088049672,
0.5384693101056830910363144207002088049672,
-0.9061798459386639927976268782993929651256,
0.9061798459386639927976268782993929651256,
],
[
0.6612093864662645136613995950199053470064,
-0.6612093864662645136613995950199053470064,
-0.2386191860831969086305017216807119354186,
0.2386191860831969086305017216807119354186,
-0.9324695142031520278123015544939946091347,
0.9324695142031520278123015544939946091347,
],
[
0, 0.4058451513773971669066064120769614633473,
-0.4058451513773971669066064120769614633473,
-0.7415311855993944398638647732807884070741,
0.7415311855993944398638647732807884070741,
-0.9491079123427585245261896840478512624007,
0.9491079123427585245261896840478512624007,
],
[
-0.1834346424956498049394761423601839806667,
0.1834346424956498049394761423601839806667,
-0.5255324099163289858177390491892463490419,
0.5255324099163289858177390491892463490419,
-0.7966664774136267395915539364758304368371,
0.7966664774136267395915539364758304368371,
-0.9602898564975362316835608685694729904282,
0.9602898564975362316835608685694729904282,
],
[
0, -0.8360311073266357942994297880697348765441,
0.8360311073266357942994297880697348765441,
-0.9681602395076260898355762029036728700494,
0.9681602395076260898355762029036728700494,
-0.3242534234038089290385380146433366085719,
0.3242534234038089290385380146433366085719,
-0.6133714327005903973087020393414741847857,
0.6133714327005903973087020393414741847857,
],
[
-0.1488743389816312108848260011297199846175,
0.1488743389816312108848260011297199846175,
-0.4333953941292471907992659431657841622,
0.4333953941292471907992659431657841622,
-0.6794095682990244062343273651148735757692,
0.6794095682990244062343273651148735757692,
-0.8650633666889845107320966884234930485275,
0.8650633666889845107320966884234930485275,
-0.9739065285171717200779640120844520534282,
0.9739065285171717200779640120844520534282,
],
[
0, -0.2695431559523449723315319854008615246796,
0.2695431559523449723315319854008615246796,
-0.5190961292068118159257256694586095544802,
0.5190961292068118159257256694586095544802,
-0.7301520055740493240934162520311534580496,
0.7301520055740493240934162520311534580496,
-0.8870625997680952990751577693039272666316,
0.8870625997680952990751577693039272666316,
-0.9782286581460569928039380011228573907714,
0.9782286581460569928039380011228573907714,
],
[
-0.1252334085114689154724413694638531299833,
0.1252334085114689154724413694638531299833,
-0.3678314989981801937526915366437175612563,
0.3678314989981801937526915366437175612563,
-0.587317954286617447296702418940534280369,
0.587317954286617447296702418940534280369,
-0.7699026741943046870368938332128180759849,
0.7699026741943046870368938332128180759849,
-0.9041172563704748566784658661190961925375,
0.9041172563704748566784658661190961925375,
-0.9815606342467192506905490901492808229601,
0.9815606342467192506905490901492808229601,
],
[
0, -0.2304583159551347940655281210979888352115,
0.2304583159551347940655281210979888352115,
-0.4484927510364468528779128521276398678019,
0.4484927510364468528779128521276398678019,
-0.6423493394403402206439846069955156500716,
0.6423493394403402206439846069955156500716,
-0.8015780907333099127942064895828598903056,
0.8015780907333099127942064895828598903056,
-0.9175983992229779652065478365007195123904,
0.9175983992229779652065478365007195123904,
-0.9841830547185881494728294488071096110649,
0.9841830547185881494728294488071096110649,
],
[
-0.1080549487073436620662446502198347476119,
0.1080549487073436620662446502198347476119,
-0.3191123689278897604356718241684754668342,
0.3191123689278897604356718241684754668342,
-0.5152486363581540919652907185511886623088,
0.5152486363581540919652907185511886623088,
-0.6872929048116854701480198030193341375384,
0.6872929048116854701480198030193341375384,
-0.8272013150697649931897947426503949610397,
0.8272013150697649931897947426503949610397,
-0.928434883663573517336391139377874264477,
0.928434883663573517336391139377874264477,
-0.986283808696812338841597266704052801676,
0.986283808696812338841597266704052801676,
],
[
0, -0.2011940939974345223006283033945962078128,
0.2011940939974345223006283033945962078128,
-0.3941513470775633698972073709810454683627,
0.3941513470775633698972073709810454683627,
-0.5709721726085388475372267372539106412383,
0.5709721726085388475372267372539106412383,
-0.7244177313601700474161860546139380096308,
0.7244177313601700474161860546139380096308,
-0.8482065834104272162006483207742168513662,
0.8482065834104272162006483207742168513662,
-0.9372733924007059043077589477102094712439,
0.9372733924007059043077589477102094712439,
-0.9879925180204854284895657185866125811469,
0.9879925180204854284895657185866125811469,
],
[
-0.0950125098376374401853193354249580631303,
0.0950125098376374401853193354249580631303,
-0.281603550779258913230460501460496106486,
0.281603550779258913230460501460496106486,
-0.45801677765722738634241944298357757354,
0.45801677765722738634241944298357757354,
-0.6178762444026437484466717640487910189918,
0.6178762444026437484466717640487910189918,
-0.7554044083550030338951011948474422683538,
0.7554044083550030338951011948474422683538,
-0.8656312023878317438804678977123931323873,
0.8656312023878317438804678977123931323873,
-0.9445750230732325760779884155346083450911,
0.9445750230732325760779884155346083450911,
-0.9894009349916499325961541734503326274262,
0.9894009349916499325961541734503326274262,
],
[
0, -0.1784841814958478558506774936540655574754,
0.1784841814958478558506774936540655574754,
-0.3512317634538763152971855170953460050405,
0.3512317634538763152971855170953460050405,
-0.5126905370864769678862465686295518745829,
0.5126905370864769678862465686295518745829,
-0.6576711592166907658503022166430023351478,
0.6576711592166907658503022166430023351478,
-0.7815140038968014069252300555204760502239,
0.7815140038968014069252300555204760502239,
-0.8802391537269859021229556944881556926234,
0.8802391537269859021229556944881556926234,
-0.9506755217687677612227169578958030214433,
0.9506755217687677612227169578958030214433,
-0.9905754753144173356754340199406652765077,
0.9905754753144173356754340199406652765077,
],
[
-0.0847750130417353012422618529357838117333,
0.0847750130417353012422618529357838117333,
-0.2518862256915055095889728548779112301628,
0.2518862256915055095889728548779112301628,
-0.4117511614628426460359317938330516370789,
0.4117511614628426460359317938330516370789,
-0.5597708310739475346078715485253291369276,
0.5597708310739475346078715485253291369276,
-0.6916870430603532078748910812888483894522,
0.6916870430603532078748910812888483894522,
-0.8037049589725231156824174550145907971032,
0.8037049589725231156824174550145907971032,
-0.8926024664975557392060605911271455154078,
0.8926024664975557392060605911271455154078,
-0.9558239495713977551811958929297763099728,
0.9558239495713977551811958929297763099728,
-0.9915651684209309467300160047061507702525,
0.9915651684209309467300160047061507702525,
],
[
0, -0.1603586456402253758680961157407435495048,
0.1603586456402253758680961157407435495048,
-0.3165640999636298319901173288498449178922,
0.3165640999636298319901173288498449178922,
-0.4645707413759609457172671481041023679762,
0.4645707413759609457172671481041023679762,
-0.6005453046616810234696381649462392798683,
0.6005453046616810234696381649462392798683,
-0.7209661773352293786170958608237816296571,
0.7209661773352293786170958608237816296571,
-0.8227146565371428249789224867127139017745,
0.8227146565371428249789224867127139017745,
-0.9031559036148179016426609285323124878093,
0.9031559036148179016426609285323124878093,
-0.960208152134830030852778840687651526615,
0.960208152134830030852778840687651526615,
-0.9924068438435844031890176702532604935893,
0.9924068438435844031890176702532604935893,
],
[
-0.0765265211334973337546404093988382110047,
0.0765265211334973337546404093988382110047,
-0.227785851141645078080496195368574624743,
0.227785851141645078080496195368574624743,
-0.3737060887154195606725481770249272373957,
0.3737060887154195606725481770249272373957,
-0.5108670019508270980043640509552509984254,
0.5108670019508270980043640509552509984254,
-0.6360536807265150254528366962262859367433,
0.6360536807265150254528366962262859367433,
-0.7463319064601507926143050703556415903107,
0.7463319064601507926143050703556415903107,
-0.8391169718222188233945290617015206853296,
0.8391169718222188233945290617015206853296,
-0.9122344282513259058677524412032981130491,
0.9122344282513259058677524412032981130491,
-0.963971927277913791267666131197277221912,
0.963971927277913791267666131197277221912,
-0.9931285991850949247861223884713202782226,
0.9931285991850949247861223884713202782226,
],
[
0, -0.1455618541608950909370309823386863301163,
0.1455618541608950909370309823386863301163,
-0.288021316802401096600792516064600319909,
0.288021316802401096600792516064600319909,
-0.4243421202074387835736688885437880520964,
0.4243421202074387835736688885437880520964,
-0.551618835887219807059018796724313286622,
0.551618835887219807059018796724313286622,
-0.667138804197412319305966669990339162597,
0.667138804197412319305966669990339162597,
-0.7684399634756779086158778513062280348209,
0.7684399634756779086158778513062280348209,
-0.8533633645833172836472506385875676702761,
0.8533633645833172836472506385875676702761,
-0.9200993341504008287901871337149688941591,
0.9200993341504008287901871337149688941591,
-0.9672268385663062943166222149076951614246,
0.9672268385663062943166222149076951614246,
-0.9937521706203895002602420359379409291933,
0.9937521706203895002602420359379409291933,
],
[
-0.0697392733197222212138417961186280818222,
0.0697392733197222212138417961186280818222,
-0.2078604266882212854788465339195457342156,
0.2078604266882212854788465339195457342156,
-0.3419358208920842251581474204273796195591,
0.3419358208920842251581474204273796195591,
-0.4693558379867570264063307109664063460953,
0.4693558379867570264063307109664063460953,
-0.5876404035069115929588769276386473488776,
0.5876404035069115929588769276386473488776,
-0.6944872631866827800506898357622567712673,
0.6944872631866827800506898357622567712673,
-0.7878168059792081620042779554083515213881,
0.7878168059792081620042779554083515213881,
-0.8658125777203001365364256370193787290847,
0.8658125777203001365364256370193787290847,
-0.9269567721871740005206929392590531966353,
0.9269567721871740005206929392590531966353,
-0.9700604978354287271239509867652687108059,
0.9700604978354287271239509867652687108059,
-0.994294585482399292073031421161298980393,
0.994294585482399292073031421161298980393,
],
[
0, -0.1332568242984661109317426822417661370104,
0.1332568242984661109317426822417661370104,
-0.264135680970344930533869538283309602979,
0.264135680970344930533869538283309602979,
-0.390301038030290831421488872880605458578,
0.390301038030290831421488872880605458578,
-0.5095014778460075496897930478668464305448,
0.5095014778460075496897930478668464305448,
-0.6196098757636461563850973116495956533871,
0.6196098757636461563850973116495956533871,
-0.7186613631319501944616244837486188483299,
0.7186613631319501944616244837486188483299,
-0.8048884016188398921511184069967785579414,
0.8048884016188398921511184069967785579414,
-0.8767523582704416673781568859341456716389,
0.8767523582704416673781568859341456716389,
-0.9329710868260161023491969890384229782357,
0.9329710868260161023491969890384229782357,
-0.9725424712181152319560240768207773751816,
0.9725424712181152319560240768207773751816,
-0.9947693349975521235239257154455743605736,
0.9947693349975521235239257154455743605736,
],
[
-0.0640568928626056260850430826247450385909,
0.0640568928626056260850430826247450385909,
-0.1911188674736163091586398207570696318404,
0.1911188674736163091586398207570696318404,
-0.3150426796961633743867932913198102407864,
0.3150426796961633743867932913198102407864,
-0.4337935076260451384870842319133497124524,
0.4337935076260451384870842319133497124524,
-0.5454214713888395356583756172183723700107,
0.5454214713888395356583756172183723700107,
-0.6480936519369755692524957869107476266696,
0.6480936519369755692524957869107476266696,
-0.7401241915785543642438281030999784255232,
0.7401241915785543642438281030999784255232,
-0.8200019859739029219539498726697452080761,
0.8200019859739029219539498726697452080761,
-0.8864155270044010342131543419821967550873,
0.8864155270044010342131543419821967550873,
-0.9382745520027327585236490017087214496548,
0.9382745520027327585236490017087214496548,
-0.9747285559713094981983919930081690617411,
0.9747285559713094981983919930081690617411,
-0.9951872199970213601799974097007368118745,
0.9951872199970213601799974097007368118745,
],
];
// Legendre-Gauss weights (wi values, defined by a function linked to in the Bezier primer article)
export const cValues = [
[],
[],
[1.0, 1.0],
[
0.8888888888888888888888888888888888888888,
0.5555555555555555555555555555555555555555,
0.5555555555555555555555555555555555555555,
],
[
0.6521451548625461426269360507780005927646,
0.6521451548625461426269360507780005927646,
0.3478548451374538573730639492219994072353,
0.3478548451374538573730639492219994072353,
],
[
0.5688888888888888888888888888888888888888,
0.4786286704993664680412915148356381929122,
0.4786286704993664680412915148356381929122,
0.2369268850561890875142640407199173626432,
0.2369268850561890875142640407199173626432,
],
[
0.3607615730481386075698335138377161116615,
0.3607615730481386075698335138377161116615,
0.4679139345726910473898703439895509948116,
0.4679139345726910473898703439895509948116,
0.1713244923791703450402961421727328935268,
0.1713244923791703450402961421727328935268,
],
[
0.4179591836734693877551020408163265306122,
0.3818300505051189449503697754889751338783,
0.3818300505051189449503697754889751338783,
0.2797053914892766679014677714237795824869,
0.2797053914892766679014677714237795824869,
0.1294849661688696932706114326790820183285,
0.1294849661688696932706114326790820183285,
],
[
0.3626837833783619829651504492771956121941,
0.3626837833783619829651504492771956121941,
0.3137066458778872873379622019866013132603,
0.3137066458778872873379622019866013132603,
0.2223810344533744705443559944262408844301,
0.2223810344533744705443559944262408844301,
0.1012285362903762591525313543099621901153,
0.1012285362903762591525313543099621901153,
],
[
0.3302393550012597631645250692869740488788,
0.1806481606948574040584720312429128095143,
0.1806481606948574040584720312429128095143,
0.0812743883615744119718921581105236506756,
0.0812743883615744119718921581105236506756,
0.3123470770400028400686304065844436655987,
0.3123470770400028400686304065844436655987,
0.2606106964029354623187428694186328497718,
0.2606106964029354623187428694186328497718,
],
[
0.295524224714752870173892994651338329421,
0.295524224714752870173892994651338329421,
0.2692667193099963550912269215694693528597,
0.2692667193099963550912269215694693528597,
0.2190863625159820439955349342281631924587,
0.2190863625159820439955349342281631924587,
0.1494513491505805931457763396576973324025,
0.1494513491505805931457763396576973324025,
0.0666713443086881375935688098933317928578,
0.0666713443086881375935688098933317928578,
],
[
0.272925086777900630714483528336342189156,
0.2628045445102466621806888698905091953727,
0.2628045445102466621806888698905091953727,
0.2331937645919904799185237048431751394317,
0.2331937645919904799185237048431751394317,
0.1862902109277342514260976414316558916912,
0.1862902109277342514260976414316558916912,
0.1255803694649046246346942992239401001976,
0.1255803694649046246346942992239401001976,
0.0556685671161736664827537204425485787285,
0.0556685671161736664827537204425485787285,
],
[
0.2491470458134027850005624360429512108304,
0.2491470458134027850005624360429512108304,
0.2334925365383548087608498989248780562594,
0.2334925365383548087608498989248780562594,
0.2031674267230659217490644558097983765065,
0.2031674267230659217490644558097983765065,
0.160078328543346226334652529543359071872,
0.160078328543346226334652529543359071872,
0.1069393259953184309602547181939962242145,
0.1069393259953184309602547181939962242145,
0.047175336386511827194615961485017060317,
0.047175336386511827194615961485017060317,
],
[
0.2325515532308739101945895152688359481566,
0.2262831802628972384120901860397766184347,
0.2262831802628972384120901860397766184347,
0.2078160475368885023125232193060527633865,
0.2078160475368885023125232193060527633865,
0.1781459807619457382800466919960979955128,
0.1781459807619457382800466919960979955128,
0.1388735102197872384636017768688714676218,
0.1388735102197872384636017768688714676218,
0.0921214998377284479144217759537971209236,
0.0921214998377284479144217759537971209236,
0.0404840047653158795200215922009860600419,
0.0404840047653158795200215922009860600419,
],
[
0.2152638534631577901958764433162600352749,
0.2152638534631577901958764433162600352749,
0.2051984637212956039659240656612180557103,
0.2051984637212956039659240656612180557103,
0.1855383974779378137417165901251570362489,
0.1855383974779378137417165901251570362489,
0.1572031671581935345696019386238421566056,
0.1572031671581935345696019386238421566056,
0.1215185706879031846894148090724766259566,
0.1215185706879031846894148090724766259566,
0.0801580871597602098056332770628543095836,
0.0801580871597602098056332770628543095836,
0.0351194603317518630318328761381917806197,
0.0351194603317518630318328761381917806197,
],
[
0.2025782419255612728806201999675193148386,
0.1984314853271115764561183264438393248186,
0.1984314853271115764561183264438393248186,
0.1861610000155622110268005618664228245062,
0.1861610000155622110268005618664228245062,
0.1662692058169939335532008604812088111309,
0.1662692058169939335532008604812088111309,
0.1395706779261543144478047945110283225208,
0.1395706779261543144478047945110283225208,
0.1071592204671719350118695466858693034155,
0.1071592204671719350118695466858693034155,
0.0703660474881081247092674164506673384667,
0.0703660474881081247092674164506673384667,
0.0307532419961172683546283935772044177217,
0.0307532419961172683546283935772044177217,
],
[
0.1894506104550684962853967232082831051469,
0.1894506104550684962853967232082831051469,
0.1826034150449235888667636679692199393835,
0.1826034150449235888667636679692199393835,
0.1691565193950025381893120790303599622116,
0.1691565193950025381893120790303599622116,
0.1495959888165767320815017305474785489704,
0.1495959888165767320815017305474785489704,
0.1246289712555338720524762821920164201448,
0.1246289712555338720524762821920164201448,
0.0951585116824927848099251076022462263552,
0.0951585116824927848099251076022462263552,
0.0622535239386478928628438369943776942749,
0.0622535239386478928628438369943776942749,
0.0271524594117540948517805724560181035122,
0.0271524594117540948517805724560181035122,
],
[
0.1794464703562065254582656442618856214487,
0.1765627053669926463252709901131972391509,
0.1765627053669926463252709901131972391509,
0.1680041021564500445099706637883231550211,
0.1680041021564500445099706637883231550211,
0.1540457610768102880814315948019586119404,
0.1540457610768102880814315948019586119404,
0.1351363684685254732863199817023501973721,
0.1351363684685254732863199817023501973721,
0.1118838471934039710947883856263559267358,
0.1118838471934039710947883856263559267358,
0.0850361483171791808835353701910620738504,
0.0850361483171791808835353701910620738504,
0.0554595293739872011294401653582446605128,
0.0554595293739872011294401653582446605128,
0.0241483028685479319601100262875653246916,
0.0241483028685479319601100262875653246916,
],
[
0.1691423829631435918406564701349866103341,
0.1691423829631435918406564701349866103341,
0.1642764837458327229860537764659275904123,
0.1642764837458327229860537764659275904123,
0.1546846751262652449254180038363747721932,
0.1546846751262652449254180038363747721932,
0.1406429146706506512047313037519472280955,
0.1406429146706506512047313037519472280955,
0.1225552067114784601845191268002015552281,
0.1225552067114784601845191268002015552281,
0.1009420441062871655628139849248346070628,
0.1009420441062871655628139849248346070628,
0.0764257302548890565291296776166365256053,
0.0764257302548890565291296776166365256053,
0.0497145488949697964533349462026386416808,
0.0497145488949697964533349462026386416808,
0.0216160135264833103133427102664524693876,
0.0216160135264833103133427102664524693876,
],
[
0.1610544498487836959791636253209167350399,
0.1589688433939543476499564394650472016787,
0.1589688433939543476499564394650472016787,
0.152766042065859666778855400897662998461,
0.152766042065859666778855400897662998461,
0.1426067021736066117757461094419029724756,
0.1426067021736066117757461094419029724756,
0.1287539625393362276755157848568771170558,
0.1287539625393362276755157848568771170558,
0.1115666455473339947160239016817659974813,
0.1115666455473339947160239016817659974813,
0.0914900216224499994644620941238396526609,
0.0914900216224499994644620941238396526609,
0.0690445427376412265807082580060130449618,
0.0690445427376412265807082580060130449618,
0.0448142267656996003328381574019942119517,
0.0448142267656996003328381574019942119517,
0.0194617882297264770363120414644384357529,
0.0194617882297264770363120414644384357529,
],
[
0.1527533871307258506980843319550975934919,
0.1527533871307258506980843319550975934919,
0.1491729864726037467878287370019694366926,
0.1491729864726037467878287370019694366926,
0.1420961093183820513292983250671649330345,
0.1420961093183820513292983250671649330345,
0.1316886384491766268984944997481631349161,
0.1316886384491766268984944997481631349161,
0.118194531961518417312377377711382287005,
0.118194531961518417312377377711382287005,
0.1019301198172404350367501354803498761666,
0.1019301198172404350367501354803498761666,
0.0832767415767047487247581432220462061001,
0.0832767415767047487247581432220462061001,
0.0626720483341090635695065351870416063516,
0.0626720483341090635695065351870416063516,
0.040601429800386941331039952274932109879,
0.040601429800386941331039952274932109879,
0.0176140071391521183118619623518528163621,
0.0176140071391521183118619623518528163621,
],
[
0.1460811336496904271919851476833711882448,
0.1445244039899700590638271665537525436099,
0.1445244039899700590638271665537525436099,
0.1398873947910731547221334238675831108927,
0.1398873947910731547221334238675831108927,
0.132268938633337461781052574496775604329,
0.132268938633337461781052574496775604329,
0.1218314160537285341953671771257335983563,
0.1218314160537285341953671771257335983563,
0.1087972991671483776634745780701056420336,
0.1087972991671483776634745780701056420336,
0.0934444234560338615532897411139320884835,
0.0934444234560338615532897411139320884835,
0.0761001136283793020170516533001831792261,
0.0761001136283793020170516533001831792261,
0.0571344254268572082836358264724479574912,
0.0571344254268572082836358264724479574912,
0.0369537897708524937999506682993296661889,
0.0369537897708524937999506682993296661889,
0.0160172282577743333242246168584710152658,
0.0160172282577743333242246168584710152658,
],
[
0.1392518728556319933754102483418099578739,
0.1392518728556319933754102483418099578739,
0.1365414983460151713525738312315173965863,
0.1365414983460151713525738312315173965863,
0.1311735047870623707329649925303074458757,
0.1311735047870623707329649925303074458757,
0.1232523768105124242855609861548144719594,
0.1232523768105124242855609861548144719594,
0.1129322960805392183934006074217843191142,
0.1129322960805392183934006074217843191142,
0.1004141444428809649320788378305362823508,
0.1004141444428809649320788378305362823508,
0.0859416062170677274144436813727028661891,
0.0859416062170677274144436813727028661891,
0.0697964684245204880949614189302176573987,
0.0697964684245204880949614189302176573987,
0.0522933351526832859403120512732112561121,
0.0522933351526832859403120512732112561121,
0.0337749015848141547933022468659129013491,
0.0337749015848141547933022468659129013491,
0.0146279952982722006849910980471854451902,
0.0146279952982722006849910980471854451902,
],
[
0.1336545721861061753514571105458443385831,
0.132462039404696617371642464703316925805,
0.132462039404696617371642464703316925805,
0.1289057221880821499785953393997936532597,
0.1289057221880821499785953393997936532597,
0.1230490843067295304675784006720096548158,
0.1230490843067295304675784006720096548158,
0.1149966402224113649416435129339613014914,
0.1149966402224113649416435129339613014914,
0.1048920914645414100740861850147438548584,
0.1048920914645414100740861850147438548584,
0.0929157660600351474770186173697646486034,
0.0929157660600351474770186173697646486034,
0.0792814117767189549228925247420432269137,
0.0792814117767189549228925247420432269137,
0.0642324214085258521271696151589109980391,
0.0642324214085258521271696151589109980391,
0.0480376717310846685716410716320339965612,
0.0480376717310846685716410716320339965612,
0.0309880058569794443106942196418845053837,
0.0309880058569794443106942196418845053837,
0.0134118594871417720813094934586150649766,
0.0134118594871417720813094934586150649766,
],
[
0.1279381953467521569740561652246953718517,
0.1279381953467521569740561652246953718517,
0.1258374563468282961213753825111836887264,
0.1258374563468282961213753825111836887264,
0.121670472927803391204463153476262425607,
0.121670472927803391204463153476262425607,
0.1155056680537256013533444839067835598622,
0.1155056680537256013533444839067835598622,
0.1074442701159656347825773424466062227946,
0.1074442701159656347825773424466062227946,
0.0976186521041138882698806644642471544279,
0.0976186521041138882698806644642471544279,
0.086190161531953275917185202983742667185,
0.086190161531953275917185202983742667185,
0.0733464814110803057340336152531165181193,
0.0733464814110803057340336152531165181193,
0.0592985849154367807463677585001085845412,
0.0592985849154367807463677585001085845412,
0.0442774388174198061686027482113382288593,
0.0442774388174198061686027482113382288593,
0.0285313886289336631813078159518782864491,
0.0285313886289336631813078159518782864491,
0.0123412297999871995468056670700372915759,
0.0123412297999871995468056670700372915759,
],
];
// LUT for binomial coefficient arrays per curve order 'n'
export const binomialCoefficients = [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1]];
export const getCubicArcLength = (xs: number[], ys: number[], t: number) => {
let sum: number;
let correctedT: number;
/*if (xs.length >= tValues.length) {
throw new Error('too high n bezier');
}*/
const n = 20;
const z = t / 2;
sum = 0;
for (let i = 0; i < n; i++) {
correctedT = z * tValues[n][i] + z;
sum += cValues[n][i] * BFunc(xs, ys, correctedT);
}
return z * sum;
};
export const getQuadraticArcLength = (
xs: number[],
ys: number[],
t: number
) => {
if (t === undefined) {
t = 1;
}
const ax = xs[0] - 2 * xs[1] + xs[2];
const ay = ys[0] - 2 * ys[1] + ys[2];
const bx = 2 * xs[1] - 2 * xs[0];
const by = 2 * ys[1] - 2 * ys[0];
const A = 4 * (ax * ax + ay * ay);
const B = 4 * (ax * bx + ay * by);
const C = bx * bx + by * by;
if (A === 0) {
return (
t * Math.sqrt(Math.pow(xs[2] - xs[0], 2) + Math.pow(ys[2] - ys[0], 2))
);
}
const b = B / (2 * A);
const c = C / A;
const u = t + b;
const k = c - b * b;
const uuk = u * u + k > 0 ? Math.sqrt(u * u + k) : 0;
const bbk = b * b + k > 0 ? Math.sqrt(b * b + k) : 0;
const term =
b + Math.sqrt(b * b + k) !== 0
? k * Math.log(Math.abs((u + uuk) / (b + bbk)))
: 0;
return (Math.sqrt(A) / 2) * (u * uuk - b * bbk + term);
};
function BFunc(xs: number[], ys: number[], t: number) {
const xbase = getDerivative(1, t, xs);
const ybase = getDerivative(1, t, ys);
const combined = xbase * xbase + ybase * ybase;
return Math.sqrt(combined);
}
/**
* Compute the curve derivative (hodograph) at t.
*/
const getDerivative = (derivative: number, t: number, vs: number[]): number => {
// the derivative of any 't'-less function is zero.
const n = vs.length - 1;
let _vs;
let value;
if (n === 0) {
return 0;
}
// direct values? compute!
if (derivative === 0) {
value = 0;
for (let k = 0; k <= n; k++) {
value +=
binomialCoefficients[n][k] *
Math.pow(1 - t, n - k) *
Math.pow(t, k) *
vs[k];
}
return value;
} else {
// Still some derivative? go down one order, then try
// for the lower order curve's.
_vs = new Array(n);
for (let k = 0; k < n; k++) {
_vs[k] = n * (vs[k + 1] - vs[k]);
}
return getDerivative(derivative - 1, t, _vs);
}
};
export const t2length = (
length: number,
totalLength: number,
func: (t: number) => number
): number => {
let error = 1;
let t = length / totalLength;
let step = (length - func(t)) / totalLength;
let numIterations = 0;
while (error > 0.001) {
const increasedTLength = func(t + step);
const increasedTError = Math.abs(length - increasedTLength) / totalLength;
if (increasedTError < error) {
error = increasedTError;
t += step;
} else {
const decreasedTLength = func(t - step);
const decreasedTError = Math.abs(length - decreasedTLength) / totalLength;
if (decreasedTError < error) {
error = decreasedTError;
t -= step;
} else {
step /= 2;
}
}
numIterations++;
if (numIterations > 500) {
break;
}
}
return t;
};
================================================
FILE: src/Canvas.ts
================================================
import { Util } from './Util.ts';
import type { Context } from './Context.ts';
import { SceneContext, HitContext } from './Context.ts';
import { Konva } from './Global.ts';
// calculate pixel ratio
let _pixelRatio;
function getDevicePixelRatio() {
if (_pixelRatio) {
return _pixelRatio;
}
const canvas = Util.createCanvasElement();
const context = canvas.getContext('2d') as any;
_pixelRatio = (function () {
const devicePixelRatio = Konva._global.devicePixelRatio || 1,
backingStoreRatio =
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio ||
1;
return devicePixelRatio / backingStoreRatio;
})();
Util.releaseCanvas(canvas);
return _pixelRatio;
}
interface ICanvasConfig {
width?: number;
height?: number;
pixelRatio?: number;
willReadFrequently?: boolean;
}
/**
* Canvas Renderer constructor. It is a wrapper around native canvas element.
* Usually you don't need to use it manually.
* @constructor
* @abstract
* @memberof Konva
* @param {Object} config
* @param {Number} config.width
* @param {Number} config.height
* @param {Number} config.pixelRatio
*/
export class Canvas {
pixelRatio = 1;
_canvas: HTMLCanvasElement;
context: Context;
width = 0;
height = 0;
isCache = false;
constructor(config: ICanvasConfig) {
const conf = config || {};
const pixelRatio =
conf.pixelRatio || Konva.pixelRatio || getDevicePixelRatio();
this.pixelRatio = pixelRatio;
this._canvas = Util.createCanvasElement();
// set inline styles
this._canvas.style.padding = '0';
this._canvas.style.margin = '0';
this._canvas.style.border = '0';
this._canvas.style.background = 'transparent';
this._canvas.style.position = 'absolute';
this._canvas.style.top = '0';
this._canvas.style.left = '0';
}
/**
* get canvas context
* @method
* @name Konva.Canvas#getContext
* @returns {CanvasContext} context
*/
getContext() {
return this.context;
}
/**
* get pixel ratio
* @method
* @name Konva.Canvas#getPixelRatio
* @returns {Number} pixel ratio
* @example
* var pixelRatio = layer.getCanvas.getPixelRatio();
*/
getPixelRatio() {
return this.pixelRatio;
}
/**
* set pixel ratio
* KonvaJS automatically handles pixel ratio adustments in order to render crisp drawings
* on all devices. Most desktops, low end tablets, and low end phones, have device pixel ratios
* of 1. Some high end tablets and phones, like iPhones and iPads have a device pixel ratio
* of 2. Some Macbook Pros, and iMacs also have a device pixel ratio of 2. Some high end Android devices have pixel
* ratios of 2 or 3. Some browsers like Firefox allow you to configure the pixel ratio of the viewport. Unless otherwise
* specificed, the pixel ratio will be defaulted to the actual device pixel ratio. You can override the device pixel
* ratio for special situations, or, if you don't want the pixel ratio to be taken into account, you can set it to 1.
* @method
* @name Konva.Canvas#setPixelRatio
* @param {Number} pixelRatio
* @example
* layer.getCanvas().setPixelRatio(3);
*/
setPixelRatio(pixelRatio) {
const previousRatio = this.pixelRatio;
this.pixelRatio = pixelRatio;
this.setSize(
this.getWidth() / previousRatio,
this.getHeight() / previousRatio
);
}
setWidth(width) {
// take into account pixel ratio
this.width = this._canvas.width = width * this.pixelRatio;
this._canvas.style.width = width + 'px';
const pixelRatio = this.pixelRatio,
_context = this.getContext()._context;
_context.scale(pixelRatio, pixelRatio);
}
setHeight(height) {
// take into account pixel ratio
this.height = this._canvas.height = height * this.pixelRatio;
this._canvas.style.height = height + 'px';
const pixelRatio = this.pixelRatio,
_context = this.getContext()._context;
_context.scale(pixelRatio, pixelRatio);
}
getWidth() {
return this.width;
}
getHeight() {
return this.height;
}
setSize(width, height) {
this.setWidth(width || 0);
this.setHeight(height || 0);
}
/**
* to data url
* @method
* @name Konva.Canvas#toDataURL
* @param {String} mimeType
* @param {Number} quality between 0 and 1 for jpg mime types
* @returns {String} data url string
*/
toDataURL(mimeType, quality) {
try {
// If this call fails (due to browser bug, like in Firefox 3.6),
// then revert to previous no-parameter image/png behavior
return this._canvas.toDataURL(mimeType, quality);
} catch (e) {
try {
return this._canvas.toDataURL();
} catch (err: any) {
Util.error(
'Unable to get data URL. ' +
err.message +
' For more info read https://konvajs.org/docs/posts/Tainted_Canvas.html.'
);
return '';
}
}
}
}
export class SceneCanvas extends Canvas {
constructor(
config: ICanvasConfig = { width: 0, height: 0, willReadFrequently: false }
) {
super(config);
this.context = new SceneContext(this, {
willReadFrequently: config.willReadFrequently,
});
this.setSize(config.width, config.height);
}
}
export class HitCanvas extends Canvas {
hitCanvas = true;
constructor(config: ICanvasConfig = { width: 0, height: 0 }) {
super(config);
this.context = new HitContext(this);
this.setSize(config.width, config.height);
}
}
================================================
FILE: src/Container.ts
================================================
import type { HitCanvas, SceneCanvas } from './Canvas.ts';
import type { SceneContext } from './Context.ts';
import { Factory } from './Factory.ts';
import type { NodeConfig } from './Node.ts';
import { Node } from './Node.ts';
import type { Shape } from './Shape.ts';
import type { GetSet, IRect } from './types.ts';
import { getNumberValidator } from './Validators.ts';
export type ClipFuncOutput =
| void
| [Path2D | CanvasFillRule]
| [Path2D, CanvasFillRule];
export interface ContainerConfig extends NodeConfig {
clearBeforeDraw?: boolean;
clipFunc?: (ctx: SceneContext) => ClipFuncOutput;
clipX?: number;
clipY?: number;
clipWidth?: number;
clipHeight?: number;
}
/**
* Container constructor. Containers are used to contain nodes or other containers
* @constructor
* @memberof Konva
* @augments Konva.Node
* @abstract
* @param {Object} config
* @@nodeParams
* @@containerParams
*/
export abstract class Container<
ChildType extends Node = Node,
Config extends ContainerConfig = ContainerConfig,
> extends Node {
children: Array = [];
/**
* returns an array of direct descendant nodes
* @method
* @name Konva.Container#getChildren
* @param {Function} [filterFunc] filter function
* @returns {Array}
* @example
* // get all children
* var children = layer.getChildren();
*
* // get only circles
* var circles = layer.getChildren(function(node){
* return node.getClassName() === 'Circle';
* });
*/
getChildren(filterFunc?: (item: Node) => boolean) {
const children = this.children || [];
if (filterFunc) {
return children.filter(filterFunc);
}
return children;
}
/**
* determine if node has children
* @method
* @name Konva.Container#hasChildren
* @returns {Boolean}
*/
hasChildren() {
return this.getChildren().length > 0;
}
/**
* remove all children. Children will be still in memory.
* If you want to completely destroy all children please use "destroyChildren" method instead
* @method
* @name Konva.Container#removeChildren
*/
removeChildren() {
this.getChildren().forEach((child) => {
// reset parent to prevent many _setChildrenIndices calls
child.parent = null;
child.index = 0;
child.remove();
});
this.children = [];
// because all children were detached from parent, request draw via container
this._requestDraw();
return this;
}
/**
* destroy all children nodes.
* @method
* @name Konva.Container#destroyChildren
*/
destroyChildren() {
this.getChildren().forEach((child) => {
// reset parent to prevent many _setChildrenIndices calls
child.parent = null;
child.index = 0;
child.destroy();
});
this.children = [];
// because all children were detached from parent, request draw via container
this._requestDraw();
return this;
}
abstract _validateAdd(node: Node): void;
/**
* add a child and children into container
* @name Konva.Container#add
* @method
* @param {...Konva.Node} children
* @returns {Container}
* @example
* layer.add(rect);
* layer.add(shape1, shape2, shape3);
* // empty arrays are accepted, though each individual child must be defined
* layer.add(...shapes);
*/
add(...children: ChildType[]) {
if (children.length === 0) {
return this;
}
if (children.length > 1) {
for (let i = 0; i < children.length; i++) {
this.add(children[i]);
}
return this;
}
const child = children[0];
if (child.getParent()) {
child.moveTo(this);
return this;
}
this._validateAdd(child);
child.index = this.getChildren().length;
child.parent = this;
child._clearCaches();
this.getChildren().push(child);
this._fire('add', {
child: child,
});
this._requestDraw();
// chainable
return this;
}
destroy() {
if (this.hasChildren()) {
this.destroyChildren();
}
super.destroy();
return this;
}
/**
* return an array of nodes that match the selector.
* You can provide a string with '#' for id selections and '.' for name selections.
* Or a function that will return true/false when a node is passed through. See example below.
* With strings you can also select by type or class name. Pass multiple selectors
* separated by a comma.
* @method
* @name Konva.Container#find
* @param {String | Function} selector
* @returns {Array}
* @example
*
* Passing a string as a selector
* // select node with id foo
* var node = stage.find('#foo');
*
* // select nodes with name bar inside layer
* var nodes = layer.find('.bar');
*
* // select all groups inside layer
* var nodes = layer.find('Group');
*
* // select all rectangles inside layer
* var nodes = layer.find('Rect');
*
* // select node with an id of foo or a name of bar inside layer
* var nodes = layer.find('#foo, .bar');
*
* Passing a function as a selector
*
* // get all groups with a function
* var groups = stage.find(node => {
* return node.getType() === 'Group';
* });
*
* // get only Nodes with partial opacity
* var alphaNodes = layer.find(node => {
* return node.getType() === 'Node' && node.getAbsoluteOpacity() < 1;
* });
*/
find(selector): Array {
// protecting _generalFind to prevent user from accidentally adding
// second argument and getting unexpected `findOne` result
return this._generalFind(selector, false);
}
/**
* return a first node from `find` method
* @method
* @name Konva.Container#findOne
* @param {String | Function} selector
* @returns {Konva.Node | Undefined}
* @example
* // select node with id foo
* var node = stage.findOne('#foo');
*
* // select node with name bar inside layer
* var nodes = layer.findOne('.bar');
*
* // select the first node to return true in a function
* var node = stage.findOne(node => {
* return node.getType() === 'Shape'
* })
*/
findOne(
selector: string | Function
): ChildNode | undefined {
const result = this._generalFind(selector, true);
return result.length > 0 ? result[0] : undefined;
}
_generalFind(
selector: string | Function,
findOne: boolean
) {
const retArr: Array = [];
this._descendants((node) => {
const valid = node._isMatch(selector);
if (valid) {
retArr.push(node as ChildNode);
}
if (valid && findOne) {
return true;
}
return false;
});
return retArr;
}
private _descendants(fn: (n: Node) => boolean) {
let shouldStop = false;
const children = this.getChildren();
for (const child of children) {
shouldStop = fn(child);
if (shouldStop) {
return true;
}
if (!child.hasChildren()) {
continue;
}
shouldStop = (child as unknown as Container)._descendants(fn);
if (shouldStop) {
return true;
}
}
return false;
}
// extenders
toObject() {
const obj = Node.prototype.toObject.call(this);
obj.children = [];
this.getChildren().forEach((child) => {
obj.children!.push(child.toObject());
});
return obj;
}
/**
* determine if node is an ancestor
* of descendant
* @method
* @name Konva.Container#isAncestorOf
* @param {Konva.Node} node
*/
isAncestorOf(node: Node) {
let parent = node.getParent();
while (parent) {
if (parent._id === this._id) {
return true;
}
parent = parent.getParent();
}
return false;
}
clone(obj?: any) {
// call super method
const node = Node.prototype.clone.call(this, obj);
this.getChildren().forEach(function (no) {
node.add(no.clone());
});
return node as this;
}
/**
* get all shapes that intersect a point. Note: because this method must clear a temporary
* canvas and redraw every shape inside the container, it should only be used for special situations
* because it performs very poorly. Please use the {@link Konva.Stage#getIntersection} method if at all possible
* because it performs much better
* nodes with listening set to false will not be detected
* @method
* @name Konva.Container#getAllIntersections
* @param {Object} pos
* @param {Number} pos.x
* @param {Number} pos.y
* @returns {Array} array of shapes
*/
getAllIntersections(pos) {
const arr: Shape[] = [];
this.find('Shape').forEach((shape) => {
if (shape.isVisible() && shape.intersects(pos)) {
arr.push(shape);
}
});
return arr;
}
_clearSelfAndDescendantCache(attr?: string) {
super._clearSelfAndDescendantCache(attr);
// skip clearing if node is cached with canvas
// for performance reasons !!!
if (this.isCached()) {
return;
}
this.children?.forEach(function (node) {
node._clearSelfAndDescendantCache(attr);
});
}
_setChildrenIndices() {
this.children?.forEach(function (child, n) {
child.index = n;
});
this._requestDraw();
}
drawScene(can?: SceneCanvas, top?: Node, bufferCanvas?: SceneCanvas) {
const layer = this.getLayer()!,
canvas = can || (layer && layer.getCanvas()),
context = canvas && canvas.getContext(),
cachedCanvas = this._getCanvasCache(),
cachedSceneCanvas = cachedCanvas && cachedCanvas.scene;
const caching = canvas && canvas.isCache;
if (!this.isVisible() && !caching) {
return this;
}
if (cachedSceneCanvas) {
context.save();
const m = this.getAbsoluteTransform(top).getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
this._drawCachedSceneCanvas(context);
context.restore();
} else {
this._drawChildren('drawScene', canvas, top, bufferCanvas);
}
return this;
}
drawHit(can?: HitCanvas, top?: Node) {
if (!this.shouldDrawHit(top)) {
return this;
}
const layer = this.getLayer()!,
canvas = can || (layer && layer.hitCanvas),
context = canvas && canvas.getContext(),
cachedCanvas = this._getCanvasCache(),
cachedHitCanvas = cachedCanvas && cachedCanvas.hit;
if (cachedHitCanvas) {
context.save();
const m = this.getAbsoluteTransform(top).getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
this._drawCachedHitCanvas(context);
context.restore();
} else {
this._drawChildren('drawHit', canvas, top);
}
return this;
}
_drawChildren(drawMethod, canvas, top, bufferCanvas?) {
const context = canvas && canvas.getContext(),
clipWidth = this.clipWidth(),
clipHeight = this.clipHeight(),
clipFunc = this.clipFunc(),
hasClip =
(typeof clipWidth === 'number' && typeof clipHeight === 'number') ||
clipFunc;
const selfCache = top === this;
if (hasClip) {
context.save();
const transform = this.getAbsoluteTransform(top);
let m = transform.getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
context.beginPath();
let clipArgs;
if (clipFunc) {
clipArgs = clipFunc.call(this, context, this);
} else {
const clipX = this.clipX();
const clipY = this.clipY();
context.rect(clipX || 0, clipY || 0, clipWidth, clipHeight);
}
context.clip.apply(context, clipArgs);
m = transform.copy().invert().getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
const hasComposition =
!selfCache &&
this.globalCompositeOperation() !== 'source-over' &&
drawMethod === 'drawScene';
if (hasComposition) {
context.save();
context._applyGlobalCompositeOperation(this);
}
this.children?.forEach(function (child) {
child[drawMethod](canvas, top, bufferCanvas);
});
if (hasComposition) {
context.restore();
}
if (hasClip) {
context.restore();
}
}
getClientRect(
config: {
skipTransform?: boolean;
skipShadow?: boolean;
skipStroke?: boolean;
relativeTo?: Container;
} = {}
): IRect {
const skipTransform = config.skipTransform;
const relativeTo = config.relativeTo;
let minX, minY, maxX, maxY;
let selfRect = {
x: Infinity,
y: Infinity,
width: 0,
height: 0,
};
const that = this;
this.children?.forEach(function (child) {
// skip invisible children
if (!child.visible()) {
return;
}
const rect = child.getClientRect({
relativeTo: that,
skipShadow: config.skipShadow,
skipStroke: config.skipStroke,
});
// skip invisible children (like empty groups)
if (rect.width === 0 && rect.height === 0) {
return;
}
if (minX === undefined) {
// initial value for first child
minX = rect.x;
minY = rect.y;
maxX = rect.x + rect.width;
maxY = rect.y + rect.height;
} else {
minX = Math.min(minX, rect.x);
minY = Math.min(minY, rect.y);
maxX = Math.max(maxX, rect.x + rect.width);
maxY = Math.max(maxY, rect.y + rect.height);
}
});
// if child is group we need to make sure it has visible shapes inside
const shapes = this.find('Shape');
let hasVisible = false;
for (let i = 0; i < shapes.length; i++) {
const shape = shapes[i];
if (shape._isVisible(this)) {
hasVisible = true;
break;
}
}
if (hasVisible && minX !== undefined) {
selfRect = {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
} else {
selfRect = {
x: 0,
y: 0,
width: 0,
height: 0,
};
}
if (!skipTransform) {
return this._transformedRect(selfRect, relativeTo);
}
return selfRect;
}
clip: GetSet;
clipX: GetSet;
clipY: GetSet;
clipWidth: GetSet;
clipHeight: GetSet;
// there was "this" instead of "Container",
// but it breaks react-konva types: https://github.com/konvajs/react-konva/issues/390
clipFunc: GetSet<
(ctx: CanvasRenderingContext2D, shape: Container) => ClipFuncOutput,
this
>;
}
// add getters setters
Factory.addComponentsGetterSetter(Container, 'clip', [
'x',
'y',
'width',
'height',
]);
/**
* get/set clip
* @method
* @name Konva.Container#clip
* @param {Object} clip
* @param {Number} clip.x
* @param {Number} clip.y
* @param {Number} clip.width
* @param {Number} clip.height
* @returns {Object}
* @example
* // get clip
* var clip = container.clip();
*
* // set clip
* container.clip({
* x: 20,
* y: 20,
* width: 20,
* height: 20
* });
*/
Factory.addGetterSetter(Container, 'clipX', undefined, getNumberValidator());
/**
* get/set clip x
* @name Konva.Container#clipX
* @method
* @param {Number} x
* @returns {Number}
* @example
* // get clip x
* var clipX = container.clipX();
*
* // set clip x
* container.clipX(10);
*/
Factory.addGetterSetter(Container, 'clipY', undefined, getNumberValidator());
/**
* get/set clip y
* @name Konva.Container#clipY
* @method
* @param {Number} y
* @returns {Number}
* @example
* // get clip y
* var clipY = container.clipY();
*
* // set clip y
* container.clipY(10);
*/
Factory.addGetterSetter(
Container,
'clipWidth',
undefined,
getNumberValidator()
);
/**
* get/set clip width
* @name Konva.Container#clipWidth
* @method
* @param {Number} width
* @returns {Number}
* @example
* // get clip width
* var clipWidth = container.clipWidth();
*
* // set clip width
* container.clipWidth(100);
*/
Factory.addGetterSetter(
Container,
'clipHeight',
undefined,
getNumberValidator()
);
/**
* get/set clip height
* @name Konva.Container#clipHeight
* @method
* @param {Number} height
* @returns {Number}
* @example
* // get clip height
* var clipHeight = container.clipHeight();
*
* // set clip height
* container.clipHeight(100);
*/
Factory.addGetterSetter(Container, 'clipFunc');
/**
* get/set clip function
* @name Konva.Container#clipFunc
* @method
* @param {Function} function
* @returns {Function}
* @example
* // get clip function
* var clipFunction = container.clipFunc();
*
* // set clip function
* container.clipFunc(function(ctx) {
* ctx.rect(0, 0, 100, 100);
* });
*
* container.clipFunc(function(ctx) {
* // optionally return a clip Path2D and clip-rule or just the clip-rule
* return [new Path2D('M0 0v50h50Z'), 'evenodd']
* });
*/
================================================
FILE: src/Context.ts
================================================
import { Util } from './Util.ts';
import { Konva } from './Global.ts';
import type { Canvas } from './Canvas.ts';
import type { Shape } from './Shape.ts';
import type { IRect } from './types.ts';
import type { Node } from './Node.ts';
function simplifyArray(arr: Array) {
const retArr: Array = [],
len = arr.length,
util = Util;
for (let n = 0; n < len; n++) {
let val = arr[n];
if (util._isNumber(val)) {
val = Math.round(val * 1000) / 1000;
} else if (!util._isString(val)) {
val = val + '';
}
retArr.push(val);
}
return retArr;
}
const COMMA = ',',
OPEN_PAREN = '(',
CLOSE_PAREN = ')',
OPEN_PAREN_BRACKET = '([',
CLOSE_BRACKET_PAREN = '])',
SEMICOLON = ';',
DOUBLE_PAREN = '()',
// EMPTY_STRING = '',
EQUALS = '=',
// SET = 'set',
CONTEXT_METHODS = [
'arc',
'arcTo',
'beginPath',
'bezierCurveTo',
'clearRect',
'clip',
'closePath',
'createLinearGradient',
'createPattern',
'createRadialGradient',
'drawImage',
'ellipse',
'fill',
'fillText',
'getImageData',
'createImageData',
'lineTo',
'moveTo',
'putImageData',
'quadraticCurveTo',
'rect',
'roundRect',
'restore',
'rotate',
'save',
'scale',
'setLineDash',
'setTransform',
'stroke',
'strokeText',
'transform',
'translate',
];
const CONTEXT_PROPERTIES = [
'fillStyle',
'strokeStyle',
'shadowColor',
'shadowBlur',
'shadowOffsetX',
'shadowOffsetY',
'letterSpacing',
'lineCap',
'lineDashOffset',
'lineJoin',
'lineWidth',
'miterLimit',
'direction',
'font',
'textAlign',
'textBaseline',
'globalAlpha',
'globalCompositeOperation',
'imageSmoothingEnabled',
'filter',
] as const;
const traceArrMax = 100;
// Check if CSS filters are supported in the current browser
let _cssFiltersSupported: boolean | null = null;
export function isCSSFiltersSupported(): boolean {
if (_cssFiltersSupported !== null) {
return _cssFiltersSupported;
}
try {
const canvas = Util.createCanvasElement();
const ctx = canvas.getContext('2d');
if (!ctx) {
_cssFiltersSupported = false;
return false;
}
return !!ctx && 'filter' in ctx;
} catch (e) {
_cssFiltersSupported = false;
return false;
}
}
interface ExtendedCanvasRenderingContext2D extends CanvasRenderingContext2D {
letterSpacing: string;
}
/**
* Konva wrapper around native 2d canvas context. It has almost the same API of 2d context with some additional functions.
* With core Konva shapes you don't need to use this object. But you will use it if you want to create
* a [custom shape](/docs/react/Custom_Shape.html) or a [custom hit regions](/docs/events/Custom_Hit_Region.html).
* For full information about each 2d context API use [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D)
* @constructor
* @memberof Konva
* @example
* const rect = new Konva.Shape({
* fill: 'red',
* width: 100,
* height: 100,
* sceneFunc: (ctx, shape) => {
* // ctx - is context wrapper
* // shape - is instance of Konva.Shape, so it equals to "rect" variable
* ctx.rect(0, 0, shape.getAttr('width'), shape.getAttr('height'));
*
* // automatically fill shape from props and draw hit region
* ctx.fillStrokeShape(shape);
* }
* })
*/
export class Context {
canvas: Canvas;
_context: CanvasRenderingContext2D;
traceArr: Array;
constructor(canvas: Canvas) {
this.canvas = canvas;
if (Konva.enableTrace) {
this.traceArr = [];
this._enableTrace();
}
}
/**
* fill shape
* @method
* @name Konva.Context#fillShape
* @param {Konva.Shape} shape
*/
fillShape(shape: Shape) {
if (shape.fillEnabled()) {
this._fill(shape);
}
}
_fill(shape: Shape) {
// abstract
}
/**
* stroke shape
* @method
* @name Konva.Context#strokeShape
* @param {Konva.Shape} shape
*/
strokeShape(shape: Shape) {
if (shape.hasStroke()) {
this._stroke(shape);
}
}
_stroke(shape: Shape) {
// abstract
}
/**
* fill then stroke
* @method
* @name Konva.Context#fillStrokeShape
* @param {Konva.Shape} shape
*/
fillStrokeShape(shape: Shape) {
if (shape.attrs.fillAfterStrokeEnabled) {
this.strokeShape(shape);
this.fillShape(shape);
} else {
this.fillShape(shape);
this.strokeShape(shape);
}
}
getTrace(relaxed?: boolean, rounded?: boolean) {
let traceArr = this.traceArr,
len = traceArr.length,
str = '',
n,
trace,
method,
args;
for (n = 0; n < len; n++) {
trace = traceArr[n];
method = trace.method;
// methods
if (method) {
args = trace.args;
str += method;
if (relaxed) {
str += DOUBLE_PAREN;
} else {
if (Util._isArray(args[0])) {
str += OPEN_PAREN_BRACKET + args.join(COMMA) + CLOSE_BRACKET_PAREN;
} else {
if (rounded) {
args = args.map((a) =>
typeof a === 'number' ? Math.floor(a) : a
);
}
str += OPEN_PAREN + args.join(COMMA) + CLOSE_PAREN;
}
}
} else {
// properties
str += trace.property;
if (!relaxed) {
str += EQUALS + trace.val;
}
}
str += SEMICOLON;
}
return str;
}
clearTrace() {
this.traceArr = [];
}
_trace(str) {
let traceArr = this.traceArr,
len;
traceArr.push(str);
len = traceArr.length;
if (len >= traceArrMax) {
traceArr.shift();
}
}
/**
* reset canvas context transform
* @method
* @name Konva.Context#reset
*/
reset() {
const pixelRatio = this.getCanvas().getPixelRatio();
this.setTransform(1 * pixelRatio, 0, 0, 1 * pixelRatio, 0, 0);
}
/**
* get canvas wrapper
* @method
* @name Konva.Context#getCanvas
* @returns {Konva.Canvas}
*/
getCanvas() {
return this.canvas;
}
/**
* clear canvas
* @method
* @name Konva.Context#clear
* @param {Object} [bounds]
* @param {Number} [bounds.x]
* @param {Number} [bounds.y]
* @param {Number} [bounds.width]
* @param {Number} [bounds.height]
*/
clear(bounds?: IRect) {
const canvas = this.getCanvas();
if (bounds) {
this.clearRect(
bounds.x || 0,
bounds.y || 0,
bounds.width || 0,
bounds.height || 0
);
} else {
this.clearRect(
0,
0,
canvas.getWidth() / canvas.pixelRatio,
canvas.getHeight() / canvas.pixelRatio
);
}
}
_applyLineCap(shape: Shape) {
const lineCap = shape.attrs.lineCap;
if (lineCap) {
this.setAttr('lineCap', lineCap);
}
}
_applyOpacity(shape: Node) {
const absOpacity = shape.getAbsoluteOpacity();
if (absOpacity !== 1) {
this.setAttr('globalAlpha', absOpacity);
}
}
_applyLineJoin(shape: Shape) {
const lineJoin = shape.attrs.lineJoin;
if (lineJoin) {
this.setAttr('lineJoin', lineJoin);
}
}
_applyMiterLimit(shape: Shape) {
const miterLimit = shape.attrs.miterLimit;
if (miterLimit != null) {
this.setAttr('miterLimit', miterLimit);
}
}
setAttr(attr: string, val) {
this._context[attr] = val;
}
/**
* arc function.
* @method
* @name Konva.Context#arc
*/
arc(
x: number,
y: number,
radius: number,
startAngle: number,
endAngle: number,
counterClockwise?: boolean
) {
this._context.arc(x, y, radius, startAngle, endAngle, counterClockwise);
}
/**
* arcTo function.
* @method
* @name Konva.Context#arcTo
*
*/
arcTo(x1: number, y1: number, x2: number, y2: number, radius: number) {
this._context.arcTo(x1, y1, x2, y2, radius);
}
/**
* beginPath function.
* @method
* @name Konva.Context#beginPath
*/
beginPath() {
this._context.beginPath();
}
/**
* bezierCurveTo function.
* @method
* @name Konva.Context#bezierCurveTo
*/
bezierCurveTo(
cp1x: number,
cp1y: number,
cp2x: number,
cp2y: number,
x: number,
y: number
) {
this._context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
}
/**
* clearRect function.
* @method
* @name Konva.Context#clearRect
*/
clearRect(x: number, y: number, width: number, height: number) {
this._context.clearRect(x, y, width, height);
}
/**
* clip function.
* @method
* @name Konva.Context#clip
*/
clip(fillRule?: CanvasFillRule): void;
clip(path: Path2D, fillRule?: CanvasFillRule): void;
clip(...args: any[]) {
this._context.clip.apply(this._context, args as any);
}
/**
* closePath function.
* @method
* @name Konva.Context#closePath
*/
closePath() {
this._context.closePath();
}
/**
* createImageData function.
* @method
* @name Konva.Context#createImageData
*/
createImageData(width, height) {
const a = arguments;
if (a.length === 2) {
return this._context.createImageData(width, height);
} else if (a.length === 1) {
return this._context.createImageData(width);
}
}
/**
* createLinearGradient function.
* @method
* @name Konva.Context#createLinearGradient
*/
createLinearGradient(x0: number, y0: number, x1: number, y1: number) {
return this._context.createLinearGradient(x0, y0, x1, y1);
}
/**
* createPattern function.
* @method
* @name Konva.Context#createPattern
*/
createPattern(image: CanvasImageSource, repetition: string | null) {
return this._context.createPattern(image, repetition);
}
/**
* createRadialGradient function.
* @method
* @name Konva.Context#createRadialGradient
*/
createRadialGradient(
x0: number,
y0: number,
r0: number,
x1: number,
y1: number,
r1: number
) {
return this._context.createRadialGradient(x0, y0, r0, x1, y1, r1);
}
/**
* drawImage function.
* @method
* @name Konva.Context#drawImage
*/
drawImage(
image: CanvasImageSource,
sx: number,
sy: number,
sWidth?: number,
sHeight?: number,
dx?: number,
dy?: number,
dWidth?: number,
dHeight?: number
) {
// this._context.drawImage(...arguments);
const a = arguments,
_context = this._context;
if (a.length === 3) {
_context.drawImage(image, sx, sy);
} else if (a.length === 5) {
_context.drawImage(image, sx, sy, sWidth as number, sHeight as number);
} else if (a.length === 9) {
_context.drawImage(
image,
sx,
sy,
sWidth as number,
sHeight as number,
dx as number,
dy as number,
dWidth as number,
dHeight as number
);
}
}
/**
* ellipse function.
* @method
* @name Konva.Context#ellipse
*/
ellipse(
x: number,
y: number,
radiusX: number,
radiusY: number,
rotation: number,
startAngle: number,
endAngle: number,
counterclockwise?: boolean
) {
this._context.ellipse(
x,
y,
radiusX,
radiusY,
rotation,
startAngle,
endAngle,
counterclockwise
);
}
/**
* isPointInPath function.
* @method
* @name Konva.Context#isPointInPath
*/
isPointInPath(
x: number,
y: number,
path?: Path2D,
fillRule?: CanvasFillRule
) {
if (path) {
return this._context.isPointInPath(path, x, y, fillRule);
}
return this._context.isPointInPath(x, y, fillRule);
}
/**
* fill function.
* @method
* @name Konva.Context#fill
*/
fill(fillRule?: CanvasFillRule): void;
fill(path: Path2D, fillRule?: CanvasFillRule): void;
fill(...args: any[]) {
// this._context.fill();
this._context.fill.apply(this._context, args as any);
}
/**
* fillRect function.
* @method
* @name Konva.Context#fillRect
*/
fillRect(x: number, y: number, width: number, height: number) {
this._context.fillRect(x, y, width, height);
}
/**
* strokeRect function.
* @method
* @name Konva.Context#strokeRect
*/
strokeRect(x: number, y: number, width: number, height: number) {
this._context.strokeRect(x, y, width, height);
}
/**
* fillText function.
* @method
* @name Konva.Context#fillText
*/
fillText(text: string, x: number, y: number, maxWidth?: number) {
if (maxWidth) {
this._context.fillText(text, x, y, maxWidth);
} else {
this._context.fillText(text, x, y);
}
}
/**
* measureText function.
* @method
* @name Konva.Context#measureText
*/
measureText(text: string) {
return this._context.measureText(text);
}
/**
* getImageData function.
* @method
* @name Konva.Context#getImageData
*/
getImageData(sx: number, sy: number, sw: number, sh: number) {
return this._context.getImageData(sx, sy, sw, sh);
}
/**
* lineTo function.
* @method
* @name Konva.Context#lineTo
*/
lineTo(x: number, y: number) {
this._context.lineTo(x, y);
}
/**
* moveTo function.
* @method
* @name Konva.Context#moveTo
*/
moveTo(x: number, y: number) {
this._context.moveTo(x, y);
}
/**
* rect function.
* @method
* @name Konva.Context#rect
*/
rect(x: number, y: number, width: number, height: number) {
this._context.rect(x, y, width, height);
}
/**
* roundRect function.
* @method
* @name Konva.Context#roundRect
*/
roundRect(
x: number,
y: number,
width: number,
height: number,
radii: number | DOMPointInit | (number | DOMPointInit)[]
) {
this._context.roundRect(x, y, width, height, radii);
}
/**
* putImageData function.
* @method
* @name Konva.Context#putImageData
*/
putImageData(imageData: ImageData, dx: number, dy: number) {
this._context.putImageData(imageData, dx, dy);
}
/**
* quadraticCurveTo function.
* @method
* @name Konva.Context#quadraticCurveTo
*/
quadraticCurveTo(cpx: number, cpy: number, x: number, y: number) {
this._context.quadraticCurveTo(cpx, cpy, x, y);
}
/**
* restore function.
* @method
* @name Konva.Context#restore
*/
restore() {
this._context.restore();
}
/**
* rotate function.
* @method
* @name Konva.Context#rotate
*/
rotate(angle: number) {
this._context.rotate(angle);
}
/**
* save function.
* @method
* @name Konva.Context#save
*/
save() {
this._context.save();
}
/**
* scale function.
* @method
* @name Konva.Context#scale
*/
scale(x: number, y: number) {
this._context.scale(x, y);
}
/**
* setLineDash function.
* @method
* @name Konva.Context#setLineDash
*/
setLineDash(segments: number[]) {
// works for Chrome and IE11
if (this._context.setLineDash) {
this._context.setLineDash(segments);
} else if ('mozDash' in this._context) {
// verified that this works in firefox
(this._context as any)['mozDash'] = segments;
} else if ('webkitLineDash' in this._context) {
// does not currently work for Safari
(this._context as any)['webkitLineDash'] = segments;
}
// no support for IE9 and IE10
}
/**
* getLineDash function.
* @method
* @name Konva.Context#getLineDash
*/
getLineDash() {
return this._context.getLineDash();
}
/**
* setTransform function.
* @method
* @name Konva.Context#setTransform
*/
setTransform(
a: number,
b: number,
c: number,
d: number,
e: number,
f: number
) {
this._context.setTransform(a, b, c, d, e, f);
}
/**
* stroke function.
* @method
* @name Konva.Context#stroke
*/
stroke(path2d?: Path2D) {
if (path2d) {
this._context.stroke(path2d);
} else {
this._context.stroke();
}
}
/**
* strokeText function.
* @method
* @name Konva.Context#strokeText
*/
strokeText(text: string, x: number, y: number, maxWidth?: number) {
this._context.strokeText(text, x, y, maxWidth);
}
/**
* transform function.
* @method
* @name Konva.Context#transform
*/
transform(a: number, b: number, c: number, d: number, e: number, f: number) {
this._context.transform(a, b, c, d, e, f);
}
/**
* translate function.
* @method
* @name Konva.Context#translate
*/
translate(x: number, y: number) {
this._context.translate(x, y);
}
_enableTrace() {
let that = this,
len = CONTEXT_METHODS.length,
origSetter = this.setAttr,
n,
args;
// to prevent creating scope function at each loop
const func = function (methodName) {
let origMethod = that[methodName],
ret;
that[methodName] = function () {
args = simplifyArray(Array.prototype.slice.call(arguments, 0));
ret = origMethod.apply(that, arguments);
that._trace({
method: methodName,
args: args,
});
return ret;
};
};
// methods
for (n = 0; n < len; n++) {
func(CONTEXT_METHODS[n]);
}
// attrs
that.setAttr = function () {
origSetter.apply(that, arguments as any);
const prop = arguments[0];
let val = arguments[1];
if (
prop === 'shadowOffsetX' ||
prop === 'shadowOffsetY' ||
prop === 'shadowBlur'
) {
val = val / this.canvas.getPixelRatio();
}
that._trace({
property: prop,
val: val,
});
};
}
_applyGlobalCompositeOperation(node) {
const op = node.attrs.globalCompositeOperation;
const def = !op || op === 'source-over';
if (!def) {
this.setAttr('globalCompositeOperation', op);
}
}
}
// supported context properties
type CanvasContextProps = Pick<
ExtendedCanvasRenderingContext2D,
(typeof CONTEXT_PROPERTIES)[number]
>;
export interface Context extends CanvasContextProps {}
CONTEXT_PROPERTIES.forEach(function (prop) {
Object.defineProperty(Context.prototype, prop, {
get() {
return this._context[prop];
},
set(val) {
this._context[prop] = val;
},
});
});
export class SceneContext extends Context {
constructor(canvas: Canvas, { willReadFrequently = false } = {}) {
super(canvas);
this._context = canvas._canvas.getContext('2d', {
willReadFrequently,
}) as CanvasRenderingContext2D;
}
_fillColor(shape: Shape) {
const fill = shape.fill();
this.setAttr('fillStyle', fill);
shape._fillFunc(this);
}
_fillPattern(shape: Shape) {
this.setAttr('fillStyle', shape._getFillPattern());
shape._fillFunc(this);
}
_fillLinearGradient(shape: Shape) {
const grd = shape._getLinearGradient();
if (grd) {
this.setAttr('fillStyle', grd);
shape._fillFunc(this);
}
}
_fillRadialGradient(shape: Shape) {
const grd = shape._getRadialGradient();
if (grd) {
this.setAttr('fillStyle', grd);
shape._fillFunc(this);
}
}
_fill(shape) {
const hasColor = shape.fill(),
fillPriority = shape.getFillPriority();
// priority fills
if (hasColor && fillPriority === 'color') {
this._fillColor(shape);
return;
}
const hasPattern = shape.getFillPatternImage();
if (hasPattern && fillPriority === 'pattern') {
this._fillPattern(shape);
return;
}
const hasLinearGradient = shape.getFillLinearGradientColorStops();
if (hasLinearGradient && fillPriority === 'linear-gradient') {
this._fillLinearGradient(shape);
return;
}
const hasRadialGradient = shape.getFillRadialGradientColorStops();
if (hasRadialGradient && fillPriority === 'radial-gradient') {
this._fillRadialGradient(shape);
return;
}
// now just try and fill with whatever is available
if (hasColor) {
this._fillColor(shape);
} else if (hasPattern) {
this._fillPattern(shape);
} else if (hasLinearGradient) {
this._fillLinearGradient(shape);
} else if (hasRadialGradient) {
this._fillRadialGradient(shape);
}
}
_strokeLinearGradient(shape) {
const start = shape.getStrokeLinearGradientStartPoint(),
end = shape.getStrokeLinearGradientEndPoint(),
colorStops = shape.getStrokeLinearGradientColorStops(),
grd = this.createLinearGradient(start.x, start.y, end.x, end.y);
if (colorStops) {
// build color stops
for (let n = 0; n < colorStops.length; n += 2) {
grd.addColorStop(colorStops[n] as number, colorStops[n + 1] as string);
}
this.setAttr('strokeStyle', grd);
}
}
_stroke(shape) {
const dash = shape.dash(),
// ignore strokeScaleEnabled for Text
strokeScaleEnabled = shape.getStrokeScaleEnabled();
if (shape.hasStroke()) {
if (!strokeScaleEnabled) {
this.save();
const pixelRatio = this.getCanvas().getPixelRatio();
this.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
}
this._applyLineCap(shape);
if (dash && shape.dashEnabled()) {
this.setLineDash(dash);
this.setAttr('lineDashOffset', shape.dashOffset());
}
this.setAttr('lineWidth', shape.strokeWidth());
if (!shape.getShadowForStrokeEnabled()) {
this.setAttr('shadowColor', 'rgba(0,0,0,0)');
}
const hasLinearGradient = shape.getStrokeLinearGradientColorStops();
if (hasLinearGradient) {
this._strokeLinearGradient(shape);
} else {
this.setAttr('strokeStyle', shape.stroke());
}
shape._strokeFunc(this);
if (!strokeScaleEnabled) {
this.restore();
}
}
}
_applyShadow(shape) {
const color = shape.getShadowRGBA() ?? 'black',
blur = shape.getShadowBlur() ?? 5,
offset = shape.getShadowOffset() ?? {
x: 0,
y: 0,
},
scale = shape.getAbsoluteScale(),
ratio = this.canvas.getPixelRatio(),
scaleX = scale.x * ratio,
scaleY = scale.y * ratio;
this.setAttr('shadowColor', color);
this.setAttr(
'shadowBlur',
blur * Math.min(Math.abs(scaleX), Math.abs(scaleY))
);
this.setAttr('shadowOffsetX', offset.x * scaleX);
this.setAttr('shadowOffsetY', offset.y * scaleY);
}
}
export class HitContext extends Context {
constructor(canvas: Canvas) {
super(canvas);
this._context = canvas._canvas.getContext('2d', {
willReadFrequently: true,
}) as CanvasRenderingContext2D;
}
_fill(shape: Shape) {
this.save();
this.setAttr('fillStyle', shape.colorKey);
shape._fillFuncHit(this);
this.restore();
}
strokeShape(shape: Shape) {
if (shape.hasHitStroke()) {
this._stroke(shape);
}
}
_stroke(shape) {
if (shape.hasHitStroke()) {
// ignore strokeScaleEnabled for Text
const strokeScaleEnabled = shape.getStrokeScaleEnabled();
if (!strokeScaleEnabled) {
this.save();
const pixelRatio = this.getCanvas().getPixelRatio();
this.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
}
this._applyLineCap(shape);
const hitStrokeWidth = shape.hitStrokeWidth();
const strokeWidth =
hitStrokeWidth === 'auto' ? shape.strokeWidth() : hitStrokeWidth;
this.setAttr('lineWidth', strokeWidth);
this.setAttr('strokeStyle', shape.colorKey);
shape._strokeFuncHit(this);
if (!strokeScaleEnabled) {
this.restore();
}
}
}
}
================================================
FILE: src/Core.ts
================================================
// enter file of limited Konva version with only core functions
export { Konva } from './_CoreInternals.ts';
import { Konva } from './_CoreInternals.ts';
export default Konva;
================================================
FILE: src/DragAndDrop.ts
================================================
import type { Container } from './Container.ts';
import { Konva } from './Global.ts';
import type { Node } from './Node.ts';
import type { Vector2d } from './types.ts';
import { Util } from './Util.ts';
export const DD = {
get isDragging() {
let flag = false;
DD._dragElements.forEach((elem) => {
if (elem.dragStatus === 'dragging') {
flag = true;
}
});
return flag;
},
justDragged: false,
get node() {
// return first dragging node
let node: Node | undefined;
DD._dragElements.forEach((elem) => {
node = elem.node;
});
return node;
},
_dragElements: new Map<
number,
{
node: Node;
startPointerPos: Vector2d;
offset: Vector2d;
pointerId?: number;
startEvent?: any;
// when we just put pointer down on a node
// it will create drag element
dragStatus: 'ready' | 'dragging' | 'stopped';
// dragStarted: boolean;
// isDragging: boolean;
// dragStopped: boolean;
}
>(),
// methods
_drag(evt) {
const nodesToFireEvents: Array = [];
DD._dragElements.forEach((elem, key) => {
const { node } = elem;
// we need to find pointer relative to that node
const stage = node.getStage()!;
stage.setPointersPositions(evt);
// it is possible that user call startDrag without any event
// it that case we need to detect first movable pointer and attach it into the node
if (elem.pointerId === undefined) {
elem.pointerId = Util._getFirstPointerId(evt);
}
const pos = stage._changedPointerPositions.find(
(pos) => pos.id === elem.pointerId
);
// not related pointer
if (!pos) {
return;
}
if (elem.dragStatus !== 'dragging') {
const dragDistance = node.dragDistance();
const distance = Math.max(
Math.abs(pos.x - elem.startPointerPos.x),
Math.abs(pos.y - elem.startPointerPos.y)
);
if (distance < dragDistance) {
return;
}
node.startDrag({ evt });
// a user can stop dragging inside `dragstart`
if (!node.isDragging()) {
return;
}
}
node._setDragPosition(evt, elem);
nodesToFireEvents.push(node);
});
// call dragmove only after ALL positions are changed
nodesToFireEvents.forEach((node) => {
// node may have been destroyed during a previous dragmove handler
if (!node.getStage()) {
return;
}
node.fire(
'dragmove',
{
type: 'dragmove',
target: node,
evt: evt,
},
true
);
});
},
// dragBefore and dragAfter allows us to set correct order of events
// setup all in dragbefore, and stop dragging only after pointerup triggered.
_endDragBefore(evt?) {
const drawNodes: Array = [];
DD._dragElements.forEach((elem) => {
const { node } = elem;
// we need to find pointer relative to that node
const stage = node.getStage()!;
if (evt) {
stage.setPointersPositions(evt);
}
const pos = stage._changedPointerPositions.find(
(pos) => pos.id === elem.pointerId
);
// that pointer is not related
if (!pos) {
return;
}
if (elem.dragStatus === 'dragging' || elem.dragStatus === 'stopped') {
// if a node is stopped manually we still need to reset events:
DD.justDragged = true;
Konva._mouseListenClick = false;
Konva._touchListenClick = false;
Konva._pointerListenClick = false;
elem.dragStatus = 'stopped';
}
const drawNode =
elem.node.getLayer() ||
((elem.node instanceof Konva['Stage'] && elem.node) as any);
if (drawNode && drawNodes.indexOf(drawNode) === -1) {
drawNodes.push(drawNode);
}
});
// draw in a sync way
// because mousemove event may trigger BEFORE batch draw is called
// but as we have not hit canvas updated yet, it will trigger incorrect mouseover/mouseout events
drawNodes.forEach((drawNode) => {
drawNode.draw();
});
},
_endDragAfter(evt) {
DD._dragElements.forEach((elem, key) => {
if (elem.dragStatus === 'stopped') {
elem.node.fire(
'dragend',
{
type: 'dragend',
target: elem.node,
evt: evt,
},
true
);
}
if (elem.dragStatus !== 'dragging') {
DD._dragElements.delete(key);
}
});
},
};
if (Konva.isBrowser) {
window.addEventListener('mouseup', DD._endDragBefore, true);
window.addEventListener('touchend', DD._endDragBefore, true);
// add touchcancel to fix this: https://github.com/konvajs/konva/issues/1843
window.addEventListener('touchcancel', DD._endDragBefore, true);
window.addEventListener('mousemove', DD._drag);
window.addEventListener('touchmove', DD._drag);
window.addEventListener('mouseup', DD._endDragAfter, false);
window.addEventListener('touchend', DD._endDragAfter, false);
window.addEventListener('touchcancel', DD._endDragAfter, false);
}
================================================
FILE: src/Factory.ts
================================================
import type { Node } from './Node.ts';
import type { GetSet } from './types.ts';
import { Util } from './Util.ts';
import { getComponentValidator } from './Validators.ts';
const GET = 'get';
const SET = 'set';
/**
* Enforces that a type is a string.
*/
type EnforceString = T extends string ? T : never;
/**
* Represents a class.
*/
type Constructor = abstract new (...args: any) => any;
/**
* An attribute of an instance of the provided class. Attributes names be strings.
*/
type Attr = EnforceString>;
/**
* A function that is called after a setter is called.
*/
type AfterFunc = (this: InstanceType) => void;
/**
* Extracts the type of a GetSet.
*/
type ExtractGetSet = T extends GetSet ? U : never;
/**
* Extracts the type of a GetSet class attribute.
*/
type Value> = ExtractGetSet<
InstanceType[U]
>;
/**
* A function that validates a value.
*/
type ValidatorFunc = (val: ExtractGetSet, attr: string) => T;
/**
* Extracts the "components" (keys) of a GetSet value. The value must be an object.
*/
type ExtractComponents> =
Value extends Record
? EnforceString>[]
: never;
export const Factory = {
addGetterSetter>(
constructor: T,
attr: U,
def?: Value,
validator?: ValidatorFunc>,
after?: AfterFunc
): void {
Factory.addGetter(constructor, attr, def);
Factory.addSetter(constructor, attr, validator, after);
Factory.addOverloadedGetterSetter(constructor, attr);
},
addGetter>(
constructor: T,
attr: U,
def?: Value
) {
const method = GET + Util._capitalize(attr);
constructor.prototype[method] =
constructor.prototype[method] ||
function (this: Node) {
const val = this.attrs[attr];
return val === undefined ? def : val;
};
},
addSetter>(
constructor: T,
attr: U,
validator?: ValidatorFunc>,
after?: AfterFunc
) {
const method = SET + Util._capitalize(attr);
if (!constructor.prototype[method]) {
Factory.overWriteSetter(constructor, attr, validator, after);
}
},
overWriteSetter>(
constructor: T,
attr: U,
validator?: ValidatorFunc>,
after?: AfterFunc
) {
const method = SET + Util._capitalize(attr);
constructor.prototype[method] = function (val) {
if (validator && val !== undefined && val !== null) {
val = validator.call(this, val, attr);
}
this._setAttr(attr, val);
if (after) {
after.call(this);
}
return this;
};
},
addComponentsGetterSetter>(
constructor: T,
attr: U,
components: ExtractComponents,
validator?: ValidatorFunc>,
after?: AfterFunc
) {
const len = components.length,
capitalize = Util._capitalize,
getter = GET + capitalize(attr),
setter = SET + capitalize(attr);
// getter
constructor.prototype[getter] = function () {
const ret: Record = {};
for (let n = 0; n < len; n++) {
const component = components[n];
ret[component] = this.getAttr(attr + capitalize(component));
}
return ret;
};
const basicValidator = getComponentValidator(components);
// setter
constructor.prototype[setter] = function (val) {
const oldVal = this.attrs[attr];
if (validator) {
val = validator.call(this, val, attr);
}
if (basicValidator) {
basicValidator.call(this, val, attr);
}
for (const key in val) {
if (!val.hasOwnProperty(key)) {
continue;
}
this._setAttr(attr + capitalize(key), val[key]);
}
if (!val) {
components.forEach((component) => {
this._setAttr(attr + capitalize(component), undefined);
});
}
this._fireChangeEvent(attr, oldVal, val);
if (after) {
after.call(this);
}
return this;
};
Factory.addOverloadedGetterSetter(constructor, attr);
},
addOverloadedGetterSetter>(
constructor: T,
attr: U
) {
const capitalizedAttr = Util._capitalize(attr),
setter = SET + capitalizedAttr,
getter = GET + capitalizedAttr;
constructor.prototype[attr] = function () {
// setting
if (arguments.length) {
this[setter](arguments[0]);
return this;
}
// getting
return this[getter]();
};
},
addDeprecatedGetterSetter>(
constructor: T,
attr: U,
def: Value,
validator: ValidatorFunc>
) {
Util.error('Adding deprecated ' + attr);
const method = GET + Util._capitalize(attr);
const message =
attr +
' property is deprecated and will be removed soon. Look at Konva change log for more information.';
constructor.prototype[method] = function () {
Util.error(message);
const val = this.attrs[attr];
return val === undefined ? def : val;
};
Factory.addSetter(constructor, attr, validator, function () {
Util.error(message);
});
Factory.addOverloadedGetterSetter(constructor, attr);
},
backCompat(
constructor: T,
methods: Record
) {
Util.each(methods, function (oldMethodName, newMethodName) {
const method = constructor.prototype[newMethodName];
const oldGetter = GET + Util._capitalize(oldMethodName);
const oldSetter = SET + Util._capitalize(oldMethodName);
function deprecated(this: Node) {
method.apply(this, arguments);
Util.error(
'"' +
oldMethodName +
'" method is deprecated and will be removed soon. Use ""' +
newMethodName +
'" instead.'
);
}
constructor.prototype[oldMethodName] = deprecated;
constructor.prototype[oldGetter] = deprecated;
constructor.prototype[oldSetter] = deprecated;
});
},
afterSetFilter(this: Node): void {
this._filterUpToDate = false;
},
};
================================================
FILE: src/FastLayer.ts
================================================
import { Util } from './Util.ts';
import { Layer } from './Layer.ts';
import { _registerNode } from './Global.ts';
/**
* FastLayer constructor. **DEPRECATED!** Please use `Konva.Layer({ listening: false})` instead. Layers are tied to their own canvas element and are used
* to contain shapes only. If you don't need node nesting, mouse and touch interactions,
* or event pub/sub, you should use FastLayer instead of Layer to create your layers.
* It renders about 2x faster than normal layers.
*
* @constructor
* @memberof Konva
* @augments Konva.Layer
@@containerParams
* @example
* var layer = new Konva.FastLayer();
*/
export class FastLayer extends Layer {
constructor(attrs) {
super(attrs);
this.listening(false);
Util.warn(
'Konva.Fast layer is deprecated. Please use "new Konva.Layer({ listening: false })" instead.'
);
}
}
FastLayer.prototype.nodeType = 'FastLayer';
_registerNode(FastLayer);
================================================
FILE: src/Global.ts
================================================
/*
* Konva JavaScript Framework v@@version
* http://konvajs.org/
* Licensed under the MIT
* Date: @@date
*
* Original work Copyright (C) 2011 - 2013 by Eric Rowell (KineticJS)
* Modified work Copyright (C) 2014 - present by Anton Lavrenov (Konva)
*
* @license
*/
const PI_OVER_180 = Math.PI / 180;
/**
* @namespace Konva
*/
function detectBrowser() {
return (
typeof window !== 'undefined' &&
// browser case
({}.toString.call(window) === '[object Window]' ||
// electron case
{}.toString.call(window) === '[object global]')
);
}
declare const WorkerGlobalScope: any;
export const glob: any =
typeof global !== 'undefined'
? global
: typeof window !== 'undefined'
? window
: typeof WorkerGlobalScope !== 'undefined'
? self
: {};
export const Konva = {
_global: glob,
version: '@@version',
isBrowser: detectBrowser(),
isUnminified: /param/.test(function (param: any) {}.toString()),
dblClickWindow: 400,
getAngle(angle: number) {
return Konva.angleDeg ? angle * PI_OVER_180 : angle;
},
enableTrace: false,
pointerEventsEnabled: true,
/**
* Should Konva automatically update canvas on any changes. Default is true.
* @property autoDrawEnabled
* @default true
* @name autoDrawEnabled
* @memberof Konva
* @example
* Konva.autoDrawEnabled = true;
*/
autoDrawEnabled: true,
/**
* Should we enable hit detection while dragging? For performance reasons, by default it is false.
* But on some rare cases you want to see hit graph and check intersections. Just set it to true.
* @property hitOnDragEnabled
* @default false
* @name hitOnDragEnabled
* @memberof Konva
* @example
* Konva.hitOnDragEnabled = true;
*/
hitOnDragEnabled: false,
/**
* Should we capture touch events and bind them to the touchstart target? That is how it works on DOM elements.
* The case: we touchstart on div1, then touchmove out of that element into another element div2.
* DOM will continue trigger touchmove events on div1 (not div2). Because events are "captured" into initial target.
* By default Konva do not do that and will trigger touchmove on another element, while pointer is moving.
* @property capturePointerEventsEnabled
* @default false
* @name capturePointerEventsEnabled
* @memberof Konva
* @example
* Konva.capturePointerEventsEnabled = true;
*/
capturePointerEventsEnabled: false,
_mouseListenClick: false,
_touchListenClick: false,
_pointerListenClick: false,
_mouseInDblClickWindow: false,
_touchInDblClickWindow: false,
_pointerInDblClickWindow: false,
_mouseDblClickPointerId: null,
_touchDblClickPointerId: null,
_pointerDblClickPointerId: null,
_renderBackend: 'web', // web, node-canvas, skia-canvas
/**
* Use legacy text rendering. with "middle" baseline by default.
* @property legacyTextRendering
* @default false
* @name legacyTextRendering
* @memberof Konva
* @example
* Konva.legacyTextRendering = true;
*/
legacyTextRendering: false,
/**
* Global pixel ratio configuration. KonvaJS automatically detect pixel ratio of current device.
* But you may override such property, if you want to use your value. Set this value before any components initializations.
* @property pixelRatio
* @default undefined
* @name pixelRatio
* @memberof Konva
* @example
* // before any Konva code:
* Konva.pixelRatio = 1;
*/
pixelRatio: (typeof window !== 'undefined' && window.devicePixelRatio) || 1,
/**
* Drag distance property. If you start to drag a node you may want to wait until pointer is moved to some distance from start point,
* only then start dragging. Default is 3px.
* @property dragDistance
* @default 3
* @memberof Konva
* @example
* Konva.dragDistance = 10;
*/
dragDistance: 3,
/**
* Use degree values for angle properties. You may set this property to false if you want to use radian values.
* @property angleDeg
* @default true
* @memberof Konva
* @example
* node.rotation(45); // 45 degrees
* Konva.angleDeg = false;
* node.rotation(Math.PI / 2); // PI/2 radian
*/
angleDeg: true,
/**
* Show different warnings about errors or wrong API usage
* @property showWarnings
* @default true
* @memberof Konva
* @example
* Konva.showWarnings = false;
*/
showWarnings: true,
/**
* Configure what mouse buttons can be used for drag and drop.
* Default value is [0, 1] - left and middle mouse buttons.
* @property dragButtons
* @default [0, 1]
* @memberof Konva
* @example
* // enable left and right mouse buttons
* Konva.dragButtons = [0, 2];
*/
dragButtons: [0, 1],
/**
* returns whether or not drag and drop is currently active
* @method
* @memberof Konva
*/
isDragging(): boolean {
return Konva['DD'].isDragging;
},
isTransforming(): boolean {
return Konva['Transformer']?.isTransforming() ?? false;
},
/**
* returns whether or not a drag and drop operation is ready, but may
* not necessarily have started
* @method
* @memberof Konva
*/
isDragReady() {
return !!Konva['DD'].node;
},
/**
* Should Konva release canvas elements on destroy. Default is true.
* Useful to avoid memory leak issues in Safari on macOS/iOS.
* @property releaseCanvasOnDestroy
* @default true
* @name releaseCanvasOnDestroy
* @memberof Konva
* @example
* Konva.releaseCanvasOnDestroy = true;
*/
releaseCanvasOnDestroy: true,
// user agent
document: glob.document,
// insert Konva into global namespace (window)
// it is required for npm packages
_injectGlobal(Konva) {
if (typeof glob.Konva !== 'undefined') {
console.error(
'Several Konva instances detected. It is not recommended to use multiple Konva instances in the same environment.'
);
}
glob.Konva = Konva;
},
};
export const _registerNode = (NodeClass: any) => {
Konva[NodeClass.prototype.getClassName()] = NodeClass;
};
Konva._injectGlobal(Konva);
================================================
FILE: src/Group.ts
================================================
import { Util } from './Util.ts';
import type { ContainerConfig } from './Container.ts';
import { Container } from './Container.ts';
import { _registerNode } from './Global.ts';
import type { Node } from './Node.ts';
import type { Shape } from './Shape.ts';
export interface GroupConfig extends ContainerConfig {}
/**
* Group constructor. Groups are used to contain shapes or other groups.
* @constructor
* @memberof Konva
* @augments Konva.Container
* @param {Object} config
* @@nodeParams
* @@containerParams
* @example
* var group = new Konva.Group();
*/
export class Group extends Container {
_validateAdd(child: Node) {
const type = child.getType();
if (type !== 'Group' && type !== 'Shape') {
Util.throw('You may only add groups and shapes to groups.');
}
}
}
Group.prototype.nodeType = 'Group';
_registerNode(Group);
================================================
FILE: src/Layer.ts
================================================
import { Util } from './Util.ts';
import type { ContainerConfig } from './Container.ts';
import { Container } from './Container.ts';
import { Node } from './Node.ts';
import { Factory } from './Factory.ts';
import { SceneCanvas, HitCanvas } from './Canvas.ts';
import type { Stage } from './Stage.ts';
import { getBooleanValidator } from './Validators.ts';
import type { GetSet, Vector2d } from './types.ts';
import type { Group } from './Group.ts';
import type { Shape } from './Shape.ts';
import { shapes } from './Shape.ts';
import { _registerNode } from './Global.ts';
export interface LayerConfig extends ContainerConfig {
clearBeforeDraw?: boolean;
hitGraphEnabled?: boolean;
imageSmoothingEnabled?: boolean;
}
// constants
const HASH = '#',
BEFORE_DRAW = 'beforeDraw',
DRAW = 'draw',
/*
* 2 - 3 - 4
* | |
* 1 - 0 5
* |
* 8 - 7 - 6
*/
INTERSECTION_OFFSETS = [
{ x: 0, y: 0 }, // 0
{ x: -1, y: -1 }, // 2
{ x: 1, y: -1 }, // 4
{ x: 1, y: 1 }, // 6
{ x: -1, y: 1 }, // 8
],
INTERSECTION_OFFSETS_LEN = INTERSECTION_OFFSETS.length;
/**
* Layer constructor. Layers are tied to their own canvas element and are used
* to contain groups or shapes.
* @constructor
* @memberof Konva
* @augments Konva.Container
* @param {Object} config
* @param {Boolean} [config.clearBeforeDraw] set this property to false if you don't want
* to clear the canvas before each layer draw. The default value is true.
* @@nodeParams
* @@containerParams
* @example
* var layer = new Konva.Layer();
* stage.add(layer);
* // now you can add shapes, groups into the layer
*/
export class Layer extends Container {
canvas = new SceneCanvas();
hitCanvas = new HitCanvas({
pixelRatio: 1,
});
_waitingForDraw = false;
constructor(config?: LayerConfig) {
super(config);
this.on('visibleChange.konva', this._checkVisibility);
this._checkVisibility();
this.on('imageSmoothingEnabledChange.konva', this._setSmoothEnabled);
this._setSmoothEnabled();
}
// for nodejs?
createPNGStream() {
const c = this.canvas._canvas as any;
return c.createPNGStream();
}
/**
* get layer canvas wrapper
* @method
* @name Konva.Layer#getCanvas
*/
getCanvas() {
return this.canvas;
}
/**
* get native canvas element
* @method
* @name Konva.Layer#getNativeCanvasElement
*/
getNativeCanvasElement() {
return this.canvas._canvas;
}
/**
* get layer hit canvas
* @method
* @name Konva.Layer#getHitCanvas
*/
getHitCanvas() {
return this.hitCanvas;
}
/**
* get layer canvas context
* @method
* @name Konva.Layer#getContext
*/
getContext() {
return this.getCanvas().getContext();
}
// TODO: deprecate this method
clear(bounds?) {
this.getContext().clear(bounds);
this.getHitCanvas().getContext().clear(bounds);
return this;
}
// extend Node.prototype.setZIndex
setZIndex(index: number) {
super.setZIndex(index);
const stage = this.getStage();
if (stage && stage.content) {
stage.content.removeChild(this.getNativeCanvasElement());
if (index < stage.children.length - 1) {
stage.content.insertBefore(
this.getNativeCanvasElement(),
stage.children[index + 1].getCanvas()._canvas
);
} else {
stage.content.appendChild(this.getNativeCanvasElement());
}
}
return this;
}
moveToTop() {
Node.prototype.moveToTop.call(this);
const stage = this.getStage();
if (stage && stage.content) {
stage.content.removeChild(this.getNativeCanvasElement());
stage.content.appendChild(this.getNativeCanvasElement());
}
return true;
}
moveUp() {
const moved = Node.prototype.moveUp.call(this);
if (!moved) {
return false;
}
const stage = this.getStage();
if (!stage || !stage.content) {
return false;
}
stage.content.removeChild(this.getNativeCanvasElement());
if (this.index < stage.children.length - 1) {
stage.content.insertBefore(
this.getNativeCanvasElement(),
stage.children[this.index + 1].getCanvas()._canvas
);
} else {
stage.content.appendChild(this.getNativeCanvasElement());
}
return true;
}
// extend Node.prototype.moveDown
moveDown() {
if (Node.prototype.moveDown.call(this)) {
const stage = this.getStage();
if (stage) {
const children = stage.children;
if (stage.content) {
stage.content.removeChild(this.getNativeCanvasElement());
stage.content.insertBefore(
this.getNativeCanvasElement(),
children[this.index + 1].getCanvas()._canvas
);
}
}
return true;
}
return false;
}
// extend Node.prototype.moveToBottom
moveToBottom() {
if (Node.prototype.moveToBottom.call(this)) {
const stage = this.getStage();
if (stage) {
const children = stage.children;
if (stage.content) {
stage.content.removeChild(this.getNativeCanvasElement());
stage.content.insertBefore(
this.getNativeCanvasElement(),
children[1].getCanvas()._canvas
);
}
}
return true;
}
return false;
}
getLayer() {
return this;
}
remove() {
const _canvas = this.getNativeCanvasElement();
Node.prototype.remove.call(this);
if (_canvas && _canvas.parentNode && Util._isInDocument(_canvas)) {
_canvas.parentNode.removeChild(_canvas);
}
return this;
}
getStage() {
return this.parent as Stage;
}
setSize({ width, height }) {
this.canvas.setSize(width, height);
this.hitCanvas.setSize(width, height);
this._setSmoothEnabled();
return this;
}
_validateAdd(child) {
const type = child.getType();
if (type !== 'Group' && type !== 'Shape') {
Util.throw('You may only add groups and shapes to a layer.');
}
}
_toKonvaCanvas(config) {
config = { ...config };
config.width = config.width || this.getWidth();
config.height = config.height || this.getHeight();
config.x = config.x !== undefined ? config.x : this.x();
config.y = config.y !== undefined ? config.y : this.y();
return Node.prototype._toKonvaCanvas.call(this, config);
}
_checkVisibility() {
const visible = this.visible();
if (visible) {
this.canvas._canvas.style.display = 'block';
} else {
this.canvas._canvas.style.display = 'none';
}
}
_setSmoothEnabled() {
this.getContext()._context.imageSmoothingEnabled =
this.imageSmoothingEnabled();
}
/**
* get/set width of layer. getter return width of stage. setter doing nothing.
* if you want change width use `stage.width(value);`
* @name Konva.Layer#width
* @method
* @returns {Number}
* @example
* var width = layer.width();
*/
getWidth() {
if (this.parent) {
return this.parent.width();
}
}
setWidth() {
Util.warn(
'Can not change width of layer. Use "stage.width(value)" function instead.'
);
}
/**
* get/set height of layer.getter return height of stage. setter doing nothing.
* if you want change height use `stage.height(value);`
* @name Konva.Layer#height
* @method
* @returns {Number}
* @example
* var height = layer.height();
*/
getHeight() {
if (this.parent) {
return this.parent.height();
}
}
setHeight() {
Util.warn(
'Can not change height of layer. Use "stage.height(value)" function instead.'
);
}
/**
* batch draw. this function will not do immediate draw
* but it will schedule drawing to next tick (requestAnimFrame)
* @method
* @name Konva.Layer#batchDraw
* @return {Konva.Layer} this
*/
batchDraw() {
if (!this._waitingForDraw) {
this._waitingForDraw = true;
Util.requestAnimFrame(() => {
this.draw();
this._waitingForDraw = false;
});
}
return this;
}
/**
* get visible intersection shape. This is the preferred
* method for determining if a point intersects a shape or not
* also you may pass optional selector parameter to return ancestor of intersected shape
* nodes with listening set to false will not be detected
* @method
* @name Konva.Layer#getIntersection
* @param {Object} pos
* @param {Number} pos.x
* @param {Number} pos.y
* @returns {Konva.Node}
* @example
* var shape = layer.getIntersection({x: 50, y: 50});
*/
getIntersection(pos: Vector2d) {
if (!this.isListening() || !this.isVisible()) {
return null;
}
// in some cases antialiased area may be bigger than 1px
// it is possible if we will cache node, then scale it a lot
let spiralSearchDistance = 1;
let continueSearch = false;
while (true) {
for (let i = 0; i < INTERSECTION_OFFSETS_LEN; i++) {
const intersectionOffset = INTERSECTION_OFFSETS[i];
const obj = this._getIntersection({
x: pos.x + intersectionOffset.x * spiralSearchDistance,
y: pos.y + intersectionOffset.y * spiralSearchDistance,
});
const shape = obj.shape;
if (shape) {
return shape;
}
// we should continue search if we found antialiased pixel
// that means our node somewhere very close
continueSearch = !!obj.antialiased;
// stop search if found empty pixel
if (!obj.antialiased) {
break;
}
}
// if no shape, and no antialiased pixel, we should end searching
if (continueSearch) {
spiralSearchDistance += 1;
} else {
return null;
}
}
}
_getIntersection(pos: Vector2d): { shape?: Shape; antialiased?: boolean } {
const ratio = this.hitCanvas.pixelRatio;
const p = this.hitCanvas.context.getImageData(
Math.round(pos.x * ratio),
Math.round(pos.y * ratio),
1,
1
).data;
const p3 = p[3];
// fully opaque pixel
if (p3 === 255) {
const colorKey = Util.getHitColorKey(p[0], p[1], p[2]);
const shape = shapes[colorKey];
if (shape) {
return {
shape: shape,
};
}
return {
antialiased: true,
};
} else if (p3 > 0) {
// antialiased pixel
return {
antialiased: true,
};
}
// empty pixel
return {};
}
drawScene(can?: SceneCanvas, top?: Node, bufferCanvas?: SceneCanvas) {
const layer = this.getLayer(),
canvas = can || (layer && layer.getCanvas());
this._fire(BEFORE_DRAW, {
node: this,
});
if (this.clearBeforeDraw()) {
canvas.getContext().clear();
}
Container.prototype.drawScene.call(this, canvas, top, bufferCanvas);
this._fire(DRAW, {
node: this,
});
return this;
}
drawHit(can?: HitCanvas, top?: Node) {
const layer = this.getLayer(),
canvas = can || (layer && layer.hitCanvas);
if (layer && layer.clearBeforeDraw()) {
layer.getHitCanvas().getContext().clear();
}
Container.prototype.drawHit.call(this, canvas, top);
return this;
}
/**
* enable hit graph. **DEPRECATED!** Use `layer.listening(true)` instead.
* @name Konva.Layer#enableHitGraph
* @method
* @returns {Layer}
*/
enableHitGraph() {
this.hitGraphEnabled(true);
return this;
}
/**
* disable hit graph. **DEPRECATED!** Use `layer.listening(false)` instead.
* @name Konva.Layer#disableHitGraph
* @method
* @returns {Layer}
*/
disableHitGraph() {
this.hitGraphEnabled(false);
return this;
}
setHitGraphEnabled(val) {
Util.warn(
'hitGraphEnabled method is deprecated. Please use layer.listening() instead.'
);
this.listening(val);
}
getHitGraphEnabled(val) {
Util.warn(
'hitGraphEnabled method is deprecated. Please use layer.listening() instead.'
);
return this.listening();
}
/**
* Show or hide hit canvas over the stage. May be useful for debugging custom hitFunc
* @name Konva.Layer#toggleHitCanvas
* @method
*/
toggleHitCanvas() {
if (!this.parent || !this.parent['content']) {
return;
}
const parent = this.parent as any;
const added = !!this.hitCanvas._canvas.parentNode;
if (added) {
parent.content.removeChild(this.hitCanvas._canvas);
} else {
parent.content.appendChild(this.hitCanvas._canvas);
}
}
destroy(): this {
Util.releaseCanvas(
this.getNativeCanvasElement(),
this.getHitCanvas()._canvas
);
return super.destroy();
}
hitGraphEnabled: GetSet;
clearBeforeDraw: GetSet;
imageSmoothingEnabled: GetSet;
}
Layer.prototype.nodeType = 'Layer';
_registerNode(Layer);
/**
* get/set imageSmoothingEnabled flag
* For more info see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingEnabled
* @name Konva.Layer#imageSmoothingEnabled
* @method
* @param {Boolean} imageSmoothingEnabled
* @returns {Boolean}
* @example
* // get imageSmoothingEnabled flag
* var imageSmoothingEnabled = layer.imageSmoothingEnabled();
*
* layer.imageSmoothingEnabled(false);
*
* layer.imageSmoothingEnabled(true);
*/
Factory.addGetterSetter(Layer, 'imageSmoothingEnabled', true);
/**
* get/set clearBeforeDraw flag which determines if the layer is cleared or not
* before drawing
* @name Konva.Layer#clearBeforeDraw
* @method
* @param {Boolean} clearBeforeDraw
* @returns {Boolean}
* @example
* // get clearBeforeDraw flag
* var clearBeforeDraw = layer.clearBeforeDraw();
*
* // disable clear before draw
* layer.clearBeforeDraw(false);
*
* // enable clear before draw
* layer.clearBeforeDraw(true);
*/
Factory.addGetterSetter(Layer, 'clearBeforeDraw', true);
Factory.addGetterSetter(Layer, 'hitGraphEnabled', true, getBooleanValidator());
/**
* get/set hitGraphEnabled flag. **DEPRECATED!** Use `layer.listening(false)` instead.
* Disabling the hit graph will greatly increase
* draw performance because the hit graph will not be redrawn each time the layer is
* drawn. This, however, also disables mouse/touch event detection
* @name Konva.Layer#hitGraphEnabled
* @method
* @param {Boolean} enabled
* @returns {Boolean}
* @example
* // get hitGraphEnabled flag
* var hitGraphEnabled = layer.hitGraphEnabled();
*
* // disable hit graph
* layer.hitGraphEnabled(false);
*
* // enable hit graph
* layer.hitGraphEnabled(true);
*/
================================================
FILE: src/Node.ts
================================================
import type { Canvas } from './Canvas.ts';
import { HitCanvas, SceneCanvas } from './Canvas.ts';
import type { Container } from './Container.ts';
import type { Context } from './Context.ts';
import { isCSSFiltersSupported } from './Context.ts';
import { DD } from './DragAndDrop.ts';
import { Factory } from './Factory.ts';
import { Konva } from './Global.ts';
import type { Layer } from './Layer.ts';
import type { Shape } from './Shape.ts';
import type { Stage } from './Stage.ts';
import type { GetSet, IRect, Vector2d } from './types.ts';
import { Transform, Util, type AnyString } from './Util.ts';
import {
getBooleanValidator,
getNumberValidator,
getStringValidator,
} from './Validators.ts';
export type FilterFunction = (this: Node, imageData: ImageData) => void;
export type Filter = FilterFunction | string;
type Filters = Array;
// CSS filter parser for fallback to function filters
function parseCSSFilters(cssFilter: string): FilterFunction {
// Parse common CSS filter functions and map to Konva filters
const filterRegex = /(\w+)\(([^)]+)\)/g;
let match;
while ((match = filterRegex.exec(cssFilter)) !== null) {
const [, filterName, filterValue] = match;
switch (filterName) {
case 'blur': {
const blurRadius = parseFloat(filterValue.replace('px', ''));
return function (imageData) {
// CSS blur uses standard deviation, Stack Blur uses radius
// Empirical testing shows CSS blur needs ~0.5 scaling for visual match
(this as any).blurRadius(blurRadius * 0.5);
// Access filters through dynamic import to avoid circular dependency
const KonvaFilters = (Konva as any).Filters;
if (KonvaFilters && KonvaFilters.Blur) {
KonvaFilters.Blur.call(this, imageData);
}
};
}
case 'brightness': {
const brightness = filterValue.includes('%')
? parseFloat(filterValue) / 100
: parseFloat(filterValue);
return function (imageData) {
(this as any).brightness(brightness); // CSS uses multiplier
const KonvaFilters = (Konva as any).Filters;
if (KonvaFilters && KonvaFilters.Brightness) {
KonvaFilters.Brightness.call(this, imageData);
}
};
}
case 'contrast': {
const contrast = parseFloat(filterValue);
return function (imageData) {
// Convert CSS contrast to Konva parameter using square root conversion
// to account for Konva's quadratic scaling: Math.pow((param + 100) / 100, 2)
const konvaContrast = 100 * (Math.sqrt(contrast) - 1);
(this as any).contrast(konvaContrast);
const KonvaFilters = (Konva as any).Filters;
if (KonvaFilters && KonvaFilters.Contrast) {
KonvaFilters.Contrast.call(this, imageData);
}
};
}
case 'grayscale': {
return function (imageData) {
const KonvaFilters = (Konva as any).Filters;
if (KonvaFilters && KonvaFilters.Grayscale) {
KonvaFilters.Grayscale.call(this, imageData);
}
};
}
case 'sepia': {
return function (imageData) {
const KonvaFilters = (Konva as any).Filters;
if (KonvaFilters && KonvaFilters.Sepia) {
KonvaFilters.Sepia.call(this, imageData);
}
};
}
case 'invert': {
return function (imageData) {
const KonvaFilters = (Konva as any).Filters;
if (KonvaFilters && KonvaFilters.Invert) {
KonvaFilters.Invert.call(this, imageData);
}
};
}
default:
Util.warn(
`CSS filter "${filterName}" is not supported in fallback mode. Consider using function filters for better compatibility.`
);
break;
}
}
return () => {};
}
type globalCompositeOperationType =
| ''
| 'source-over'
| 'source-in'
| 'source-out'
| 'source-atop'
| 'destination-over'
| 'destination-in'
| 'destination-out'
| 'destination-atop'
| 'lighter'
| 'copy'
| 'xor'
| 'multiply'
| 'screen'
| 'overlay'
| 'darken'
| 'lighten'
| 'color-dodge'
| 'color-burn'
| 'hard-light'
| 'soft-light'
| 'difference'
| 'exclusion'
| 'hue'
| 'saturation'
| 'color'
| 'luminosity';
// allow any custom attribute
export type NodeConfig = {
x?: number;
y?: number;
width?: number;
height?: number;
visible?: boolean;
listening?: boolean;
id?: string;
name?: string;
opacity?: number;
scale?: Vector2d;
scaleX?: number;
skewX?: number;
skewY?: number;
scaleY?: number;
rotation?: number;
rotationDeg?: number;
offset?: Vector2d;
offsetX?: number;
offsetY?: number;
draggable?: boolean;
dragDistance?: number;
dragBoundFunc?: (this: Node, pos: Vector2d) => Vector2d;
preventDefault?: boolean;
globalCompositeOperation?: globalCompositeOperationType;
filters?: Filters;
[string: string]: any;
};
// CONSTANTS
const ABSOLUTE_OPACITY = 'absoluteOpacity',
ALL_LISTENERS = 'allEventListeners',
ABSOLUTE_TRANSFORM = 'absoluteTransform',
ABSOLUTE_SCALE = 'absoluteScale',
CANVAS = 'canvas',
CHANGE = 'Change',
CHILDREN = 'children',
KONVA = 'konva',
LISTENING = 'listening',
MOUSEENTER = 'mouseenter',
MOUSELEAVE = 'mouseleave',
POINTERENTER = 'pointerenter',
POINTERLEAVE = 'pointerleave',
TOUCHENTER = 'touchenter',
TOUCHLEAVE = 'touchleave',
NAME = 'name',
SET = 'set',
SHAPE = 'Shape',
SPACE = ' ',
STAGE = 'stage',
TRANSFORM = 'transform',
UPPER_STAGE = 'Stage',
VISIBLE = 'visible',
TRANSFORM_CHANGE_STR = [
'xChange.konva',
'yChange.konva',
'scaleXChange.konva',
'scaleYChange.konva',
'skewXChange.konva',
'skewYChange.konva',
'rotationChange.konva',
'offsetXChange.konva',
'offsetYChange.konva',
'transformsEnabledChange.konva',
].join(SPACE);
let idCounter = 1;
// create all the events here
type NodeEventMap = GlobalEventHandlersEventMap & {
[index: string]: any;
};
export interface KonvaEventObject {
type: string;
target: Shape | Stage;
evt: EventType;
pointerId: number;
currentTarget: This;
cancelBubble: boolean;
child?: Node;
}
export type KonvaEventListener = (
this: This,
ev: KonvaEventObject
) => void;
export type CanvasConfig = {
x?: number;
y?: number;
width?: number;
height?: number;
pixelRatio?: number;
imageSmoothingEnabled?: boolean;
};
export type ImageConfig = CanvasConfig & {
mimeType?: string;
quality?: number;
};
/**
* Node constructor. Nodes are entities that can be transformed, layered,
* and have bound events. The stage, layers, groups, and shapes all extend Node.
* @constructor
* @memberof Konva
* @param {Object} config
* @@nodeParams
*/
export abstract class Node {
_id = idCounter++;
eventListeners: {
[index: string]: Array<{ name: string; handler: Function }>;
} = {};
attrs: any = {};
index = 0;
_allEventListeners: null | Array = null;
parent: Container | null = null;
_cache: Map = new Map();
_attachedDepsListeners: Map = new Map();
_lastPos: Vector2d | null = null;
_attrsAffectingSize!: string[];
_batchingTransformChange = false;
_needClearTransformCache = false;
_filterUpToDate = false;
_isUnderCache = false;
nodeType!: string;
className!: string;
_dragEventId: number | null = null;
_shouldFireChangeEvents = false;
constructor(config?: Config) {
// on initial set attrs wi don't need to fire change events
// because nobody is listening to them yet
this.setAttrs(config);
this._shouldFireChangeEvents = true;
// all change event listeners are attached to the prototype
}
hasChildren() {
return false;
}
_clearCache(attr?: string) {
// if we want to clear transform cache
// we don't really need to remove it from the cache
// but instead mark as "dirty"
// so we don't need to create a new instance next time
if (
(attr === TRANSFORM || attr === ABSOLUTE_TRANSFORM) &&
this._cache.get(attr)
) {
(this._cache.get(attr) as Transform).dirty = true;
} else if (attr) {
this._cache.delete(attr);
} else {
this._cache.clear();
}
}
_getCache(attr: string, privateGetter: Function) {
let cache = this._cache.get(attr);
// for transform the cache can be NOT empty
// but we still need to recalculate it if it is dirty
const isTransform = attr === TRANSFORM || attr === ABSOLUTE_TRANSFORM;
const invalid =
cache === undefined || (isTransform && cache.dirty === true);
// if not cached, we need to set it using the private getter method.
if (invalid) {
cache = privateGetter.call(this);
this._cache.set(attr, cache);
}
return cache;
}
_calculate(name: string, deps: Array, getter: Function) {
// if we are trying to calculate function for the first time
// we need to attach listeners for change events
if (!this._attachedDepsListeners.get(name)) {
const depsString = deps.map((dep) => dep + 'Change.konva').join(SPACE);
this.on(depsString, () => {
this._clearCache(name);
});
this._attachedDepsListeners.set(name, true);
}
// just use cache function
return this._getCache(name, getter);
}
_getCanvasCache() {
return this._cache.get(CANVAS);
}
/*
* when the logic for a cached result depends on ancestor propagation, use this
* method to clear self and children cache
*/
_clearSelfAndDescendantCache(attr?: string) {
this._clearCache(attr);
// trigger clear cache, so transformer can use it
if (attr === ABSOLUTE_TRANSFORM) {
this.fire('absoluteTransformChange');
}
}
/**
* clear cached canvas
* @method
* @name Konva.Node#clearCache
* @returns {Konva.Node}
* @example
* node.clearCache();
*/
clearCache() {
if (this._cache.has(CANVAS)) {
const { scene, filter, hit } = this._cache.get(CANVAS);
Util.releaseCanvas(scene._canvas, filter._canvas, hit._canvas);
this._cache.delete(CANVAS);
}
this._clearSelfAndDescendantCache();
this._requestDraw();
return this;
}
/**
* cache node to improve drawing performance, apply filters, or create more accurate
* hit regions. For all basic shapes size of cache canvas will be automatically detected.
* If you need to cache your custom `Konva.Shape` instance you have to pass shape's bounding box
* properties. Look at [https://konvajs.org/docs/performance/Shape_Caching.html](https://konvajs.org/docs/performance/Shape_Caching.html) for more information.
* @method
* @name Konva.Node#cache
* @param {Object} [config]
* @param {Number} [config.x]
* @param {Number} [config.y]
* @param {Number} [config.width]
* @param {Number} [config.height]
* @param {Number} [config.offset] increase canvas size by `offset` pixel in all directions.
* @param {Boolean} [config.drawBorder] when set to true, a red border will be drawn around the cached
* region for debugging purposes
* @param {Number} [config.pixelRatio] change quality (or pixel ratio) of cached image. pixelRatio = 2 will produce 2x sized cache.
* @param {Boolean} [config.imageSmoothingEnabled] control imageSmoothingEnabled property of created canvas for cache
* @param {Number} [config.hitCanvasPixelRatio] change quality (or pixel ratio) of cached hit canvas.
* @returns {Konva.Node}
* @example
* // cache a shape with the x,y position of the bounding box at the center and
* // the width and height of the bounding box equal to the width and height of
* // the shape obtained from shape.width() and shape.height()
* image.cache();
*
* // cache a node and define the bounding box position and size
* node.cache({
* x: -30,
* y: -30,
* width: 100,
* height: 200
* });
*
* // cache a node and draw a red border around the bounding box
* // for debugging purposes
* node.cache({
* x: -30,
* y: -30,
* width: 100,
* height: 200,
* offset : 10,
* drawBorder: true
* });
*/
cache(
config?: CanvasConfig & {
drawBorder?: boolean;
offset?: number;
hitCanvasPixelRatio?: number;
}
) {
const conf = config || {};
let rect = {} as IRect;
// don't call getClientRect if we have all attributes
// it means call it only if have one undefined
if (
conf.x === undefined ||
conf.y === undefined ||
conf.width === undefined ||
conf.height === undefined
) {
rect = this.getClientRect({
skipTransform: true,
relativeTo: this.getParent() || undefined,
});
}
let width = Math.ceil(conf.width || rect.width),
height = Math.ceil(conf.height || rect.height),
pixelRatio = conf.pixelRatio,
x = conf.x === undefined ? Math.floor(rect.x) : conf.x,
y = conf.y === undefined ? Math.floor(rect.y) : conf.y,
offset = conf.offset || 0,
drawBorder = conf.drawBorder || false,
hitCanvasPixelRatio = conf.hitCanvasPixelRatio || 1;
if (!width || !height) {
Util.error(
'Can not cache the node. Width or height of the node equals 0. Caching is skipped.'
);
return;
}
// because using Math.floor on x, y position may shift drawing
// to avoid shift we need to increase size
// but we better to avoid it, for better filters flows
const extraPaddingX = Math.abs(Math.round(rect.x) - x) > 0.5 ? 1 : 0;
const extraPaddingY = Math.abs(Math.round(rect.y) - y) > 0.5 ? 1 : 0;
width += offset * 2 + extraPaddingX;
height += offset * 2 + extraPaddingY;
x -= offset;
y -= offset;
// if (Math.floor(x) < x) {
// x = Math.floor(x);
// // width += 1;
// }
// if (Math.floor(y) < y) {
// y = Math.floor(y);
// // height += 1;
// }
// console.log({ x, y, width, height }, rect);
const cachedSceneCanvas = new SceneCanvas({
pixelRatio: pixelRatio,
width: width,
height: height,
}),
cachedFilterCanvas = new SceneCanvas({
pixelRatio: pixelRatio,
width: 0,
height: 0,
willReadFrequently: true,
}),
cachedHitCanvas = new HitCanvas({
pixelRatio: hitCanvasPixelRatio,
width: width,
height: height,
}),
sceneContext = cachedSceneCanvas.getContext(),
hitContext = cachedHitCanvas.getContext();
const bufferCanvas = new SceneCanvas({
// width and height already multiplied by pixelRatio
// so we need to revert that
// also increase size by x nd y offset to make sure content fits canvas
width:
cachedSceneCanvas.width / cachedSceneCanvas.pixelRatio + Math.abs(x),
height:
cachedSceneCanvas.height / cachedSceneCanvas.pixelRatio + Math.abs(y),
pixelRatio: cachedSceneCanvas.pixelRatio,
}),
bufferContext = bufferCanvas.getContext();
cachedHitCanvas.isCache = true;
cachedSceneCanvas.isCache = true;
this._cache.delete(CANVAS);
this._filterUpToDate = false;
if (conf.imageSmoothingEnabled === false) {
cachedSceneCanvas.getContext()._context.imageSmoothingEnabled = false;
cachedFilterCanvas.getContext()._context.imageSmoothingEnabled = false;
}
sceneContext.save();
hitContext.save();
bufferContext.save();
sceneContext.translate(-x, -y);
hitContext.translate(-x, -y);
bufferContext.translate(-x, -y);
// hard-code offset to make sure content fits canvas
// @ts-ignore
bufferCanvas.x = x;
// @ts-ignore
bufferCanvas.y = y;
// extra flag to skip on getAbsolute opacity calc
this._isUnderCache = true;
this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY);
this._clearSelfAndDescendantCache(ABSOLUTE_SCALE);
this.drawScene(cachedSceneCanvas, this, bufferCanvas);
this.drawHit(cachedHitCanvas, this);
this._isUnderCache = false;
sceneContext.restore();
hitContext.restore();
// this will draw a red border around the cached box for
// debugging purposes
if (drawBorder) {
sceneContext.save();
sceneContext.beginPath();
sceneContext.rect(0, 0, width, height);
sceneContext.closePath();
sceneContext.setAttr('strokeStyle', 'red');
sceneContext.setAttr('lineWidth', 5);
sceneContext.stroke();
sceneContext.restore();
}
// Release buffer canvas immediately - it's only needed during initial cache drawing
// This significantly reduces memory usage for cached nodes
Util.releaseCanvas(bufferCanvas._canvas);
this._cache.set(CANVAS, {
scene: cachedSceneCanvas,
filter: cachedFilterCanvas,
hit: cachedHitCanvas,
x: x,
y: y,
});
this._requestDraw();
return this;
}
/**
* determine if node is currently cached
* @method
* @name Konva.Node#isCached
* @returns {Boolean}
*/
isCached() {
return this._cache.has(CANVAS);
}
abstract drawScene(canvas?: Canvas, top?: Node, bufferCanvas?: Canvas): void;
abstract drawHit(canvas?: Canvas, top?: Node): void;
/**
* Return client rectangle {x, y, width, height} of node. This rectangle also include all styling (strokes, shadows, etc).
* The purpose of the method is similar to getBoundingClientRect API of the DOM.
* @method
* @name Konva.Node#getClientRect
* @param {Object} config
* @param {Boolean} [config.skipTransform] should we apply transform to node for calculating rect?
* @param {Boolean} [config.skipShadow] should we apply shadow to the node for calculating bound box?
* @param {Boolean} [config.skipStroke] should we apply stroke to the node for calculating bound box?
* @param {Object} [config.relativeTo] calculate client rect relative to one of the parents
* @returns {Object} rect with {x, y, width, height} properties
* @example
* var rect = new Konva.Rect({
* width : 100,
* height : 100,
* x : 50,
* y : 50,
* strokeWidth : 4,
* stroke : 'black',
* offsetX : 50,
* scaleY : 2
* });
*
* // get client rect without think off transformations (position, rotation, scale, offset, etc)
* rect.getClientRect({ skipTransform: true});
* // returns {
* // x : -2, // two pixels for stroke / 2
* // y : -2,
* // width : 104, // increased by 4 for stroke
* // height : 104
* //}
*
* // get client rect with transformation applied
* rect.getClientRect();
* // returns Object {x: -2, y: 46, width: 104, height: 208}
*/
getClientRect(config?: {
skipTransform?: boolean;
skipShadow?: boolean;
skipStroke?: boolean;
relativeTo?: Container;
}): { x: number; y: number; width: number; height: number } {
// abstract method
// redefine in Container and Shape
throw new Error('abstract "getClientRect" method call');
}
_transformedRect(rect: IRect, top?: Node | null) {
const points = [
{ x: rect.x, y: rect.y },
{ x: rect.x + rect.width, y: rect.y },
{ x: rect.x + rect.width, y: rect.y + rect.height },
{ x: rect.x, y: rect.y + rect.height },
];
let minX: number = Infinity,
minY: number = Infinity,
maxX: number = -Infinity,
maxY: number = -Infinity;
const trans = this.getAbsoluteTransform(top);
points.forEach(function (point) {
const transformed = trans.point(point);
if (minX === undefined) {
minX = maxX = transformed.x;
minY = maxY = transformed.y;
}
minX = Math.min(minX, transformed.x);
minY = Math.min(minY, transformed.y);
maxX = Math.max(maxX, transformed.x);
maxY = Math.max(maxY, transformed.y);
});
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}
_drawCachedSceneCanvas(context: Context) {
context.save();
context._applyOpacity(this);
context._applyGlobalCompositeOperation(this);
const canvasCache = this._getCanvasCache();
context.translate(canvasCache.x, canvasCache.y);
const cacheCanvas = this._getCachedSceneCanvas();
const ratio = cacheCanvas.pixelRatio;
context.drawImage(
cacheCanvas._canvas,
0,
0,
cacheCanvas.width / ratio,
cacheCanvas.height / ratio
);
context.restore();
}
_drawCachedHitCanvas(context: Context) {
const canvasCache = this._getCanvasCache(),
hitCanvas = canvasCache.hit;
context.save();
context.translate(canvasCache.x, canvasCache.y);
context.drawImage(
hitCanvas._canvas,
0,
0,
hitCanvas.width / hitCanvas.pixelRatio,
hitCanvas.height / hitCanvas.pixelRatio
);
context.restore();
}
_getCachedSceneCanvas() {
let filters = this.filters(),
cachedCanvas = this._getCanvasCache(),
sceneCanvas = cachedCanvas.scene as Canvas,
filterCanvas = cachedCanvas.filter as Canvas,
filterContext = filterCanvas.getContext(),
len,
imageData,
n,
filter;
if (!filters || filters.length === 0) {
return sceneCanvas;
}
if (this._filterUpToDate) {
return filterCanvas;
}
let useNativeOnly = true;
for (let i = 0; i < filters.length; i++) {
const fallbackRequired =
typeof filters[i] === 'string' && !isCSSFiltersSupported();
if (fallbackRequired) {
// Util.warn(
// `CSS filter "${filters[i]}" is not supported in native mode.`
// );
}
if (typeof filters[i] !== 'string' || !isCSSFiltersSupported()) {
useNativeOnly = false;
break;
}
}
const ratio = sceneCanvas.pixelRatio;
filterCanvas.setSize(
sceneCanvas.width / sceneCanvas.pixelRatio,
sceneCanvas.height / sceneCanvas.pixelRatio
);
if (useNativeOnly) {
const finalFilter = (filters as unknown as string[]).join(' ');
filterContext.save();
filterContext.setAttr('filter', finalFilter);
filterContext.drawImage(
sceneCanvas._canvas,
0,
0,
sceneCanvas.getWidth() / ratio,
sceneCanvas.getHeight() / ratio
);
filterContext.restore();
this._filterUpToDate = true;
return filterCanvas;
}
try {
len = filters.length;
filterContext.clear();
// copy cached canvas onto filter context
filterContext.drawImage(
sceneCanvas._canvas,
0,
0,
sceneCanvas.getWidth() / ratio,
sceneCanvas.getHeight() / ratio
);
imageData = filterContext.getImageData(
0,
0,
filterCanvas.getWidth(),
filterCanvas.getHeight()
);
// apply filters to filter context
for (n = 0; n < len; n++) {
filter = filters[n];
if (typeof filter === 'string') {
filter = parseCSSFilters(filter);
}
filter.call(this, imageData);
filterContext.putImageData(imageData, 0, 0);
}
} catch (e: any) {
Util.error(
'Unable to apply filter. ' +
e.message +
' This post my help you https://konvajs.org/docs/posts/Tainted_Canvas.html.'
);
}
this._filterUpToDate = true;
return filterCanvas;
}
/**
* bind events to the node. KonvaJS supports mouseover, mousemove,
* mouseout, mouseenter, mouseleave, mousedown, mouseup, wheel, contextmenu, click, dblclick, touchstart, touchmove,
* touchend, tap, dbltap, dragstart, dragmove, and dragend events.
* Pass in a string of events delimited by a space to bind multiple events at once
* such as 'mousedown mouseup mousemove'. Include a namespace to bind an
* event by name such as 'click.foobar'.
* @method
* @name Konva.Node#on
* @param {String} evtStr e.g. 'click', 'mousedown touchstart', 'mousedown.foo touchstart.foo'
* @param {Function} handler The handler function. The first argument of that function is event object. Event object has `target` as main target of the event, `currentTarget` as current node listener and `evt` as native browser event.
* @returns {Konva.Node}
* @example
* // add click listener
* node.on('click', function() {
* console.log('you clicked me!');
* });
*
* // get the target node
* node.on('click', function(evt) {
* console.log(evt.target);
* });
*
* // stop event propagation
* node.on('click', function(evt) {
* evt.cancelBubble = true;
* });
*
* // bind multiple listeners
* node.on('click touchstart', function() {
* console.log('you clicked/touched me!');
* });
*
* // namespace listener
* node.on('click.foo', function() {
* console.log('you clicked/touched me!');
* });
*
* // get the event type
* node.on('click tap', function(evt) {
* var eventType = evt.type;
* });
*
* // get native event object
* node.on('click tap', function(evt) {
* var nativeEvent = evt.evt;
* });
*
* // for change events, get the old and new val
* node.on('xChange', function(evt) {
* var oldVal = evt.oldVal;
* var newVal = evt.newVal;
* });
*
* // get event targets
* // with event delegations
* layer.on('click', 'Group', function(evt) {
* var shape = evt.target;
* var group = evt.currentTarget;
* });
*/
on(
evtStr: K,
handler: KonvaEventListener
): this;
on(
evtStr: K,
selector: string,
handler: KonvaEventListener
): this;
on(...args: any[]): this {
const evtStr = args[0];
const selectorOrHandler = args[1];
const handler = args[2];
if (this._cache) {
this._cache.delete(ALL_LISTENERS);
}
if (args.length === 3) {
return this._delegate.apply(this, args as any);
}
const events = (evtStr as string).split(SPACE);
/*
* loop through types and attach event listeners to
* each one. eg. 'click mouseover.namespace mouseout'
* will create three event bindings
*/
for (let n = 0; n < events.length; n++) {
const event = events[n];
const parts = event.split('.');
const baseEvent = parts[0];
const name = parts[1] || '';
// create events array if it doesn't exist
if (!this.eventListeners[baseEvent]) {
this.eventListeners[baseEvent] = [];
}
this.eventListeners[baseEvent].push({ name, handler: selectorOrHandler });
}
return this;
}
/**
* remove event bindings from the node. Pass in a string of
* event types delimmited by a space to remove multiple event
* bindings at once such as 'mousedown mouseup mousemove'.
* include a namespace to remove an event binding by name
* such as 'click.foobar'. If you only give a name like '.foobar',
* all events in that namespace will be removed.
* @method
* @name Konva.Node#off
* @param {String} evtStr e.g. 'click', 'mousedown touchstart', '.foobar'
* @returns {Konva.Node}
* @example
* // remove listener
* node.off('click');
*
* // remove multiple listeners
* node.off('click touchstart');
*
* // remove listener by name
* node.off('click.foo');
*/
off(evtStr?: string, callback?: Function) {
let events = (evtStr || '').split(SPACE),
len = events.length,
n,
t,
event,
parts,
baseEvent,
name;
this._cache && this._cache.delete(ALL_LISTENERS);
if (!evtStr) {
// remove all events
for (t in this.eventListeners) {
this._off(t);
}
}
for (n = 0; n < len; n++) {
event = events[n];
parts = event.split('.');
baseEvent = parts[0];
name = parts[1];
if (baseEvent) {
if (this.eventListeners[baseEvent]) {
this._off(baseEvent, name, callback);
}
} else {
for (t in this.eventListeners) {
this._off(t, name, callback);
}
}
}
return this;
}
// some event aliases for third party integration like HammerJS
dispatchEvent(evt: any) {
const e = {
target: this,
type: evt.type,
evt: evt,
};
this.fire(evt.type, e);
return this;
}
addEventListener(type: string, handler: (e: Event) => void) {
// we have to pass native event to handler
this.on(type, function (evt) {
handler.call(this, evt.evt);
});
return this;
}
removeEventListener(type: string) {
this.off(type);
return this;
}
// like node.on
_delegate(event: string, selector: string, handler: (e: Event) => void) {
const stopNode = this;
this.on(event, function (evt) {
const targets = evt.target.findAncestors(selector, true, stopNode);
for (let i = 0; i < targets.length; i++) {
evt = Util.cloneObject(evt);
evt.currentTarget = targets[i] as any;
handler.call(targets[i], evt as any);
}
});
return this;
}
/**
* remove a node from parent, but don't destroy. You can reuse the node later.
* @method
* @name Konva.Node#remove
* @returns {Konva.Node}
* @example
* node.remove();
*/
remove() {
if (this.isDragging()) {
this.stopDrag();
}
// we can have drag element but that is not dragged yet
// so just clear it
DD._dragElements.delete(this._id);
// also remove any dragging descendants, otherwise they'll
// have a broken parent chain and crash on getStage()
DD._dragElements.forEach((elem, key) => {
if (this.isAncestorOf(elem.node)) {
DD._dragElements.delete(key);
}
});
this._remove();
return this;
}
_clearCaches() {
this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM);
this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY);
this._clearSelfAndDescendantCache(ABSOLUTE_SCALE);
this._clearSelfAndDescendantCache(STAGE);
this._clearSelfAndDescendantCache(VISIBLE);
this._clearSelfAndDescendantCache(LISTENING);
}
_remove() {
// every cached attr that is calculated via node tree
// traversal must be cleared when removing a node
this._clearCaches();
const parent = this.getParent();
if (parent && parent.children) {
parent.children.splice(this.index, 1);
parent._setChildrenIndices();
this.parent = null;
}
}
/**
* remove and destroy a node. Kill it and delete forever! You should not reuse node after destroy().
* If the node is a container (Group, Stage or Layer) it will destroy all children too.
* @method
* @name Konva.Node#destroy
* @example
* node.destroy();
*/
destroy() {
this.remove();
this.clearCache();
return this;
}
/**
* get attr
* @method
* @name Konva.Node#getAttr
* @param {String} attr
* @returns {Integer|String|Object|Array}
* @example
* var x = node.getAttr('x');
*/
getAttr>(
attr: K
): K extends keyof AttrConfig ? AttrConfig[K] : any {
const method = 'get' + Util._capitalize(attr as string);
if (Util._isFunction((this as any)[method])) {
return (this as any)[method]();
}
// otherwise get directly
return this.attrs[attr];
}
/**
* get ancestors
* @method
* @name Konva.Node#getAncestors
* @returns {Array}
* @example
* shape.getAncestors().forEach(function(node) {
* console.log(node.id());
* })
*/
getAncestors() {
let parent = this.getParent(),
ancestors: Array = [];
while (parent) {
ancestors.push(parent);
parent = parent.getParent();
}
return ancestors;
}
/**
* get attrs object literal
* @method
* @name Konva.Node#getAttrs
* @returns {Object}
*/
getAttrs(): Config {
return this.attrs || {};
}
/**
* set multiple attrs at once using an object literal
* @method
* @name Konva.Node#setAttrs
* @param {Object} config object containing key value pairs
* @returns {Konva.Node}
* @example
* node.setAttrs({
* x: 5,
* fill: 'red'
* });
*/
setAttrs(config?: Config) {
this._batchTransformChanges(() => {
let key, method;
if (!config) {
return this;
}
for (key in config) {
if (key === CHILDREN) {
continue;
}
method = SET + Util._capitalize(key);
// use setter if available
if (Util._isFunction(this[method])) {
this[method](config[key]);
} else {
// otherwise set directly
this._setAttr(key, config[key]);
}
}
});
return this;
}
/**
* determine if node is listening for events by taking into account ancestors.
*
* Parent | Self | isListening
* listening | listening |
* ----------+-----------+------------
* T | T | T
* T | F | F
* F | T | F
* F | F | F
*
* @method
* @name Konva.Node#isListening
* @returns {Boolean}
*/
isListening() {
return this._getCache(LISTENING, this._isListening);
}
_isListening(relativeTo?: Node): boolean {
const listening = this.listening();
if (!listening) {
return false;
}
const parent = this.getParent();
if (parent && parent !== relativeTo && this !== relativeTo) {
return parent._isListening(relativeTo);
} else {
return true;
}
}
/**
* determine if node is visible by taking into account ancestors.
*
* Parent | Self | isVisible
* visible | visible |
* ----------+-----------+------------
* T | T | T
* T | F | F
* F | T | F
* F | F | F
* @method
* @name Konva.Node#isVisible
* @returns {Boolean}
*/
isVisible() {
return this._getCache(VISIBLE, this._isVisible);
}
_isVisible(relativeTo?: Node): boolean {
const visible = this.visible();
if (!visible) {
return false;
}
const parent = this.getParent();
if (parent && parent !== relativeTo && this !== relativeTo) {
return parent._isVisible(relativeTo);
} else {
return true;
}
}
shouldDrawHit(top?: Node, skipDragCheck = false) {
if (top) {
return this._isVisible(top) && this._isListening(top);
}
const layer = this.getLayer();
let layerUnderDrag = false;
DD._dragElements.forEach((elem) => {
if (elem.dragStatus !== 'dragging') {
return;
} else if (elem.node.nodeType === 'Stage') {
layerUnderDrag = true;
} else if (elem.node.getLayer() === layer) {
layerUnderDrag = true;
}
});
const dragSkip =
!skipDragCheck &&
!Konva.hitOnDragEnabled &&
(layerUnderDrag || Konva.isTransforming());
return this.isListening() && this.isVisible() && !dragSkip;
}
/**
* show node. set visible = true
* @method
* @name Konva.Node#show
* @returns {Konva.Node}
*/
show() {
this.visible(true);
return this;
}
/**
* hide node. Hidden nodes are no longer detectable
* @method
* @name Konva.Node#hide
* @returns {Konva.Node}
*/
hide() {
this.visible(false);
return this;
}
getZIndex() {
return this.index || 0;
}
/**
* get absolute z-index which takes into account sibling
* and ancestor indices
* @method
* @name Konva.Node#getAbsoluteZIndex
* @returns {Integer}
*/
getAbsoluteZIndex() {
let depth = this.getDepth(),
that = this,
index = 0,
nodes,
len,
n,
child;
function addChildren(children) {
nodes = [];
len = children.length;
for (n = 0; n < len; n++) {
child = children[n];
index++;
if (child.nodeType !== SHAPE) {
nodes = nodes.concat(child.getChildren().slice());
}
if (child._id === that._id) {
n = len;
}
}
if (nodes.length > 0 && nodes[0].getDepth() <= depth) {
addChildren(nodes);
}
}
const stage = this.getStage();
if (that.nodeType !== UPPER_STAGE && stage) {
addChildren(stage.getChildren());
}
return index;
}
/**
* get node depth in node tree. Returns an integer.
* e.g. Stage depth will always be 0. Layers will always be 1. Groups and Shapes will always
* be >= 2
* @method
* @name Konva.Node#getDepth
* @returns {Integer}
*/
getDepth() {
let depth = 0,
parent = this.parent;
while (parent) {
depth++;
parent = parent.parent;
}
return depth;
}
// sometimes we do several attributes changes
// like node.position(pos)
// for performance reasons, lets batch transform reset
// so it work faster
_batchTransformChanges(func) {
this._batchingTransformChange = true;
func();
this._batchingTransformChange = false;
if (this._needClearTransformCache) {
this._clearCache(TRANSFORM);
this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM);
}
this._needClearTransformCache = false;
}
setPosition(pos: Vector2d) {
this._batchTransformChanges(() => {
this.x(pos.x);
this.y(pos.y);
});
return this;
}
getPosition() {
return {
x: this.x(),
y: this.y(),
};
}
/**
* get position of first pointer (like mouse or first touch) relative to local coordinates of current node
* @method
* @name Konva.Node#getRelativePointerPosition
* @returns {Konva.Node}
* @example
*
* // let's think we have a rectangle at position x = 10, y = 10
* // now we clicked at x = 15, y = 15 of the stage
* // if you want to know position of the click, related to the rectangle you can use
* rect.getRelativePointerPosition();
*/
getRelativePointerPosition() {
const stage = this.getStage();
if (!stage) {
return null;
}
// get pointer (say mouse or touch) position
const pos = stage.getPointerPosition();
if (!pos) {
return null;
}
const transform = this.getAbsoluteTransform().copy();
// to detect relative position we need to invert transform
transform.invert();
// now we can find relative point
return transform.point(pos);
}
/**
* get absolute position of a node. That function can be used to calculate absolute position, but relative to any ancestor
* @method
* @name Konva.Node#getAbsolutePosition
* @param {Object} Ancestor optional ancestor node
* @returns {Konva.Node}
* @example
*
* // returns absolute position relative to top-left corner of canvas
* node.getAbsolutePosition();
*
* // calculate absolute position of node, inside stage
* // so stage transforms are ignored
* node.getAbsolutePosition(stage)
*/
getAbsolutePosition(top?: Node) {
let haveCachedParent = false;
let parent = this.parent;
while (parent) {
if (parent.isCached()) {
haveCachedParent = true;
break;
}
parent = parent.parent;
}
if (haveCachedParent && !top) {
// make fake top element
// "true" is not a node, but it will just allow skip all caching
top = true as any;
}
const absoluteMatrix = this.getAbsoluteTransform(top).getMatrix(),
absoluteTransform = new Transform(),
offset = this.offset();
// clone the matrix array
absoluteTransform.m = absoluteMatrix.slice();
absoluteTransform.translate(offset.x, offset.y);
return absoluteTransform.getTranslation();
}
setAbsolutePosition(pos: Vector2d) {
const { x, y, ...origTrans } = this._clearTransform();
// don't clear translation
this.attrs.x = x;
this.attrs.y = y;
// important, use non cached value
this._clearCache(TRANSFORM);
const it = this._getAbsoluteTransform().copy();
it.invert();
it.translate(pos.x, pos.y);
pos = {
x: this.attrs.x + it.getTranslation().x,
y: this.attrs.y + it.getTranslation().y,
};
this._setTransform(origTrans);
this.setPosition({ x: pos.x, y: pos.y });
this._clearCache(TRANSFORM);
this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM);
return this;
}
_setTransform(trans) {
let key;
for (key in trans) {
this.attrs[key] = trans[key];
}
// this._clearCache(TRANSFORM);
// this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM);
}
_clearTransform() {
const trans = {
x: this.x(),
y: this.y(),
rotation: this.rotation(),
scaleX: this.scaleX(),
scaleY: this.scaleY(),
offsetX: this.offsetX(),
offsetY: this.offsetY(),
skewX: this.skewX(),
skewY: this.skewY(),
};
this.attrs.x = 0;
this.attrs.y = 0;
this.attrs.rotation = 0;
this.attrs.scaleX = 1;
this.attrs.scaleY = 1;
this.attrs.offsetX = 0;
this.attrs.offsetY = 0;
this.attrs.skewX = 0;
this.attrs.skewY = 0;
// return original transform
return trans;
}
/**
* move node by an amount relative to its current position
* @method
* @name Konva.Node#move
* @param {Object} change
* @param {Number} change.x
* @param {Number} change.y
* @returns {Konva.Node}
* @example
* // move node in x direction by 1px and y direction by 2px
* node.move({
* x: 1,
* y: 2
* });
*/
move(change: Vector2d) {
let changeX = change.x,
changeY = change.y,
x = this.x(),
y = this.y();
if (changeX !== undefined) {
x += changeX;
}
if (changeY !== undefined) {
y += changeY;
}
this.setPosition({ x: x, y: y });
return this;
}
_eachAncestorReverse(func, top) {
let family: Array = [],
parent = this.getParent(),
len,
n;
// if top node is defined, and this node is top node,
// there's no need to build a family tree. just execute
// func with this because it will be the only node
if (top && top._id === this._id) {
// func(this);
return;
}
family.unshift(this);
while (parent && (!top || parent._id !== top._id)) {
family.unshift(parent);
parent = parent.parent;
}
len = family.length;
for (n = 0; n < len; n++) {
func(family[n]);
}
}
/**
* rotate node by an amount in degrees relative to its current rotation
* @method
* @name Konva.Node#rotate
* @param {Number} theta
* @returns {Konva.Node}
*/
rotate(theta: number) {
this.rotation(this.rotation() + theta);
return this;
}
/**
* move node to the top of its siblings
* @method
* @name Konva.Node#moveToTop
* @returns {Boolean}
*/
moveToTop() {
if (!this.parent) {
Util.warn('Node has no parent. moveToTop function is ignored.');
return false;
}
const index = this.index,
len = this.parent.getChildren().length;
if (index < len - 1) {
this.parent.children.splice(index, 1);
this.parent.children.push(this);
this.parent._setChildrenIndices();
return true;
}
return false;
}
/**
* move node up
* @method
* @name Konva.Node#moveUp
* @returns {Boolean} flag is moved or not
*/
moveUp() {
if (!this.parent) {
Util.warn('Node has no parent. moveUp function is ignored.');
return false;
}
const index = this.index,
len = this.parent.getChildren().length;
if (index < len - 1) {
this.parent.children.splice(index, 1);
this.parent.children.splice(index + 1, 0, this);
this.parent._setChildrenIndices();
return true;
}
return false;
}
/**
* move node down
* @method
* @name Konva.Node#moveDown
* @returns {Boolean}
*/
moveDown() {
if (!this.parent) {
Util.warn('Node has no parent. moveDown function is ignored.');
return false;
}
const index = this.index;
if (index > 0) {
this.parent.children.splice(index, 1);
this.parent.children.splice(index - 1, 0, this);
this.parent._setChildrenIndices();
return true;
}
return false;
}
/**
* move node to the bottom of its siblings
* @method
* @name Konva.Node#moveToBottom
* @returns {Boolean}
*/
moveToBottom() {
if (!this.parent) {
Util.warn('Node has no parent. moveToBottom function is ignored.');
return false;
}
const index = this.index;
if (index > 0) {
this.parent.children.splice(index, 1);
this.parent.children.unshift(this);
this.parent._setChildrenIndices();
return true;
}
return false;
}
setZIndex(zIndex) {
if (!this.parent) {
Util.warn('Node has no parent. zIndex parameter is ignored.');
return this;
}
if (zIndex < 0 || zIndex >= this.parent.children.length) {
Util.warn(
'Unexpected value ' +
zIndex +
' for zIndex property. zIndex is just index of a node in children of its parent. Expected value is from 0 to ' +
(this.parent.children.length - 1) +
'.'
);
}
const index = this.index;
this.parent.children.splice(index, 1);
this.parent.children.splice(zIndex, 0, this);
this.parent._setChildrenIndices();
return this;
}
/**
* get absolute opacity
* @method
* @name Konva.Node#getAbsoluteOpacity
* @returns {Number}
*/
getAbsoluteOpacity() {
return this._getCache(ABSOLUTE_OPACITY, this._getAbsoluteOpacity);
}
_getAbsoluteOpacity() {
let absOpacity = this.opacity();
const parent = this.getParent();
if (parent && !parent._isUnderCache) {
absOpacity *= parent.getAbsoluteOpacity();
}
return absOpacity;
}
/**
* move node to another container
* @method
* @name Konva.Node#moveTo
* @param {Container} newContainer
* @returns {Konva.Node}
* @example
* // move node from current layer into layer2
* node.moveTo(layer2);
*/
moveTo(newContainer: any) {
// do nothing if new container is already parent
if (this.getParent() !== newContainer) {
this._remove();
newContainer.add(this);
}
return this;
}
/**
* convert Node into an object for serialization. Returns an object.
* @method
* @name Konva.Node#toObject
* @returns {Object}
*/
toObject() {
let attrs = this.getAttrs(),
key,
val,
getter,
defaultValue,
nonPlainObject;
const obj: {
attrs: Config & Record;
className: string;
children?: Array;
} = {
attrs: {} as Config & Record,
className: this.getClassName(),
};
for (key in attrs) {
val = attrs[key];
// if value is object and object is not plain
// like class instance, we should skip it and to not include
nonPlainObject =
Util.isObject(val) && !Util._isPlainObject(val) && !Util._isArray(val);
if (nonPlainObject) {
continue;
}
getter = typeof this[key] === 'function' && this[key];
// remove attr value so that we can extract the default value from the getter
delete attrs[key];
defaultValue = getter ? getter.call(this) : null;
// restore attr value
(attrs as any)[key] = val;
if (defaultValue !== val) {
(obj.attrs as any)[key] = val;
}
}
return Util._prepareToStringify(obj) as typeof obj;
}
/**
* convert Node into a JSON string. Returns a JSON string.
* @method
* @name Konva.Node#toJSON
* @returns {String}
*/
toJSON() {
return JSON.stringify(this.toObject());
}
/**
* get parent container
* @method
* @name Konva.Node#getParent
* @returns {Konva.Node}
*/
getParent() {
return this.parent;
}
/**
* get all ancestors (parent then parent of the parent, etc) of the node
* @method
* @name Konva.Node#findAncestors
* @param {String} selector selector for search
* @param {Boolean} [includeSelf] show we think that node is ancestro itself?
* @param {Konva.Node} [stopNode] optional node where we need to stop searching (one of ancestors)
* @returns {Array} [ancestors]
* @example
* // get one of the parent group
* var parentGroups = node.findAncestors('Group');
*/
findAncestors(
selector: string | Function,
includeSelf?: boolean,
stopNode?: Node
) {
const res: Array = [];
if (includeSelf && this._isMatch(selector)) {
res.push(this);
}
let ancestor = this.parent;
while (ancestor) {
if (ancestor === stopNode) {
return res;
}
if (ancestor._isMatch(selector)) {
res.push(ancestor);
}
ancestor = ancestor.parent;
}
return res;
}
isAncestorOf(node: Node) {
return false;
}
/**
* get ancestor (parent or parent of the parent, etc) of the node that match passed selector
* @method
* @name Konva.Node#findAncestor
* @param {String} selector selector for search
* @param {Boolean} [includeSelf] show we think that node is ancestro itself?
* @param {Konva.Node} [stopNode] optional node where we need to stop searching (one of ancestors)
* @returns {Konva.Node} ancestor
* @example
* // get one of the parent group
* var group = node.findAncestors('.mygroup');
*/
findAncestor(
selector: string | Function,
includeSelf?: boolean,
stopNode?: Container
) {
return this.findAncestors(selector, includeSelf, stopNode)[0];
}
// is current node match passed selector?
_isMatch(selector: string | Function) {
if (!selector) {
return false;
}
if (typeof selector === 'function') {
return selector(this);
}
let selectorArr = selector.replace(/ /g, '').split(','),
len = selectorArr.length,
n,
sel;
for (n = 0; n < len; n++) {
sel = selectorArr[n];
if (!Util.isValidSelector(sel)) {
Util.warn(
'Selector "' +
sel +
'" is invalid. Allowed selectors examples are "#foo", ".bar" or "Group".'
);
Util.warn(
'If you have a custom shape with such className, please change it to start with upper letter like "Triangle".'
);
Util.warn('Konva is awesome, right?');
}
// id selector
if (sel.charAt(0) === '#') {
if (this.id() === sel.slice(1)) {
return true;
}
} else if (sel.charAt(0) === '.') {
// name selector
if (this.hasName(sel.slice(1))) {
return true;
}
} else if (this.className === sel || this.nodeType === sel) {
return true;
}
}
return false;
}
/**
* get layer ancestor
* @method
* @name Konva.Node#getLayer
* @returns {Konva.Layer}
*/
getLayer(): Layer | null {
const parent = this.getParent();
return parent ? parent.getLayer() : null;
}
/**
* get stage ancestor
* @method
* @name Konva.Node#getStage
* @returns {Konva.Stage}
*/
getStage(): Stage | null {
return this._getCache(STAGE, this._getStage);
}
_getStage() {
const parent = this.getParent();
if (parent) {
return parent.getStage();
} else {
return null;
}
}
/**
* fire event
* @method
* @name Konva.Node#fire
* @param {String} eventType event type. can be a regular event, like click, mouseover, or mouseout, or it can be a custom event, like myCustomEvent
* @param {Event} [evt] event object
* @param {Boolean} [bubble] setting the value to false, or leaving it undefined, will result in the event
* not bubbling. Setting the value to true will result in the event bubbling.
* @returns {Konva.Node}
* @example
* // manually fire click event
* node.fire('click');
*
* // fire custom event
* node.fire('foo');
*
* // fire custom event with custom event object
* node.fire('foo', {
* bar: 10
* });
*
* // fire click event that bubbles
* node.fire('click', null, true);
*/
fire(eventType: string, evt: any = {}, bubble?: boolean) {
evt.target = evt.target || this;
// bubble
if (bubble) {
this._fireAndBubble(eventType, evt);
} else {
// no bubble
this._fire(eventType, evt);
}
return this;
}
/**
* get absolute transform of the node which takes into
* account its ancestor transforms
* @method
* @name Konva.Node#getAbsoluteTransform
* @returns {Konva.Transform}
*/
getAbsoluteTransform(top?: Node | null) {
// if using an argument, we can't cache the result.
if (top) {
return this._getAbsoluteTransform(top);
} else {
// if no argument, we can cache the result
return this._getCache(
ABSOLUTE_TRANSFORM,
this._getAbsoluteTransform
) as Transform;
}
}
_getAbsoluteTransform(top?: Node) {
let at: Transform;
// we we need position relative to an ancestor, we will iterate for all
if (top) {
at = new Transform();
// start with stage and traverse downwards to self
this._eachAncestorReverse(function (node: Node) {
const transformsEnabled = node.transformsEnabled();
if (transformsEnabled === 'all') {
at.multiply(node.getTransform());
} else if (transformsEnabled === 'position') {
at.translate(node.x() - node.offsetX(), node.y() - node.offsetY());
}
}, top);
return at;
} else {
// try to use a cached value
at = this._cache.get(ABSOLUTE_TRANSFORM) || new Transform();
if (this.parent) {
// transform will be cached
this.parent.getAbsoluteTransform().copyInto(at);
} else {
at.reset();
}
const transformsEnabled = this.transformsEnabled();
if (transformsEnabled === 'all') {
at.multiply(this.getTransform());
} else if (transformsEnabled === 'position') {
// use "attrs" directly, because it is a bit faster
const x = this.attrs.x || 0;
const y = this.attrs.y || 0;
const offsetX = this.attrs.offsetX || 0;
const offsetY = this.attrs.offsetY || 0;
at.translate(x - offsetX, y - offsetY);
}
at.dirty = false;
return at;
}
}
/**
* get absolute scale of the node which takes into
* account its ancestor scales
* @method
* @name Konva.Node#getAbsoluteScale
* @returns {Object}
* @example
* // get absolute scale x
* var scaleX = node.getAbsoluteScale().x;
*/
getAbsoluteScale(top?: Node) {
// do not cache this calculations,
// because it use cache transform
// this is special logic for caching with some shapes with shadow
let parent: Node | null = this;
while (parent) {
if (parent._isUnderCache) {
top = parent;
}
parent = parent.getParent();
}
const transform = this.getAbsoluteTransform(top);
const attrs = transform.decompose();
return {
x: attrs.scaleX,
y: attrs.scaleY,
};
}
/**
* get absolute rotation of the node which takes into
* account its ancestor rotations
* @method
* @name Konva.Node#getAbsoluteRotation
* @returns {Number}
* @example
* // get absolute rotation
* var rotation = node.getAbsoluteRotation();
*/
getAbsoluteRotation() {
// var parent: Node = this;
// var rotation = 0;
// while (parent) {
// rotation += parent.rotation();
// parent = parent.getParent();
// }
// return rotation;
return this.getAbsoluteTransform().decompose().rotation;
}
/**
* get transform of the node
* @method
* @name Konva.Node#getTransform
* @returns {Konva.Transform}
*/
getTransform() {
return this._getCache(TRANSFORM, this._getTransform) as Transform;
}
_getTransform(): Transform {
const m: Transform = this._cache.get(TRANSFORM) || new Transform();
m.reset();
// I was trying to use attributes directly here
// but it doesn't work for Transformer well
// because it overwrite x,y getters
const x = this.x(),
y = this.y(),
rotation = Konva.getAngle(this.rotation()),
scaleX = this.attrs.scaleX ?? 1,
scaleY = this.attrs.scaleY ?? 1,
skewX = this.attrs.skewX || 0,
skewY = this.attrs.skewY || 0,
offsetX = this.attrs.offsetX || 0,
offsetY = this.attrs.offsetY || 0;
if (x !== 0 || y !== 0) {
m.translate(x, y);
}
if (rotation !== 0) {
m.rotate(rotation);
}
if (skewX !== 0 || skewY !== 0) {
m.skew(skewX, skewY);
}
if (scaleX !== 1 || scaleY !== 1) {
m.scale(scaleX, scaleY);
}
if (offsetX !== 0 || offsetY !== 0) {
m.translate(-1 * offsetX, -1 * offsetY);
}
m.dirty = false;
return m;
}
/**
* clone node. Returns a new Node instance with identical attributes. You can also override
* the node properties with an object literal, enabling you to use an existing node as a template
* for another node
* @method
* @name Konva.Node#clone
* @param {Object} obj override attrs
* @returns {Konva.Node}
* @example
* // simple clone
* var clone = node.clone();
*
* // clone a node and override the x position
* var clone = rect.clone({
* x: 5
* });
*/
clone(obj?: any) {
// instantiate new node
let attrs = Util.cloneObject(this.attrs),
key,
allListeners,
len,
n,
listener;
// apply attr overrides
for (key in obj) {
attrs[key] = obj[key];
}
const node = new (this.constructor as any)(attrs);
// copy over listeners
for (key in this.eventListeners) {
allListeners = this.eventListeners[key];
len = allListeners.length;
for (n = 0; n < len; n++) {
listener = allListeners[n];
/*
* don't include konva namespaced listeners because
* these are generated by the constructors
*/
if (listener.name.indexOf(KONVA) < 0) {
// if listeners array doesn't exist, then create it
if (!node.eventListeners[key]) {
node.eventListeners[key] = [];
}
node.eventListeners[key].push(listener);
}
}
}
return node;
}
_toKonvaCanvas(config) {
config = config || {};
const box = this.getClientRect();
const stage = this.getStage(),
x = config.x !== undefined ? config.x : Math.floor(box.x),
y = config.y !== undefined ? config.y : Math.floor(box.y),
pixelRatio = config.pixelRatio || 1,
canvas = new SceneCanvas({
width:
config.width || Math.ceil(box.width) || (stage ? stage.width() : 0),
height:
config.height ||
Math.ceil(box.height) ||
(stage ? stage.height() : 0),
pixelRatio: pixelRatio,
}),
context = canvas.getContext();
const bufferCanvas = new SceneCanvas({
// width and height already multiplied by pixelRatio
// so we need to revert that
// also increase size by x nd y offset to make sure content fits canvas
width: canvas.width / canvas.pixelRatio + Math.abs(x),
height: canvas.height / canvas.pixelRatio + Math.abs(y),
pixelRatio: canvas.pixelRatio,
});
if (config.imageSmoothingEnabled === false) {
context._context.imageSmoothingEnabled = false;
}
context.save();
if (x || y) {
context.translate(-1 * x, -1 * y);
}
this.drawScene(canvas, undefined, bufferCanvas);
context.restore();
return canvas;
}
/**
* converts node into an canvas element.
* @method
* @name Konva.Node#toCanvas
* @param {Object} config
* @param {Function} config.callback function executed when the composite has completed
* @param {Number} [config.x] x position of canvas section
* @param {Number} [config.y] y position of canvas section
* @param {Number} [config.width] width of canvas section
* @param {Number} [config.height] height of canvas section
* @param {Number} [config.pixelRatio] pixelRatio of output canvas. Default is 1.
* You can use that property to increase quality of the image, for example for super hight quality exports
* or usage on retina (or similar) displays. pixelRatio will be used to multiply the size of exported image.
* If you export to 500x500 size with pixelRatio = 2, then produced image will have size 1000x1000.
* @param {Boolean} [config.imageSmoothingEnabled] set this to false if you want to disable imageSmoothing
* @example
* var canvas = node.toCanvas();
*/
toCanvas(config?: CanvasConfig) {
return this._toKonvaCanvas(config)._canvas;
}
/**
* Creates a composite data URL (base64 string). If MIME type is not
* specified, then "image/png" will result. For "image/jpeg", specify a quality
* level as quality (range 0.0 - 1.0)
* @method
* @name Konva.Node#toDataURL
* @param {Object} config
* @param {String} [config.mimeType] can be "image/png" or "image/jpeg".
* "image/png" is the default
* @param {Number} [config.x] x position of canvas section
* @param {Number} [config.y] y position of canvas section
* @param {Number} [config.width] width of canvas section
* @param {Number} [config.height] height of canvas section
* @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType,
* you can specify the quality from 0 to 1, where 0 is very poor quality and 1
* is very high quality
* @param {Number} [config.pixelRatio] pixelRatio of output image url. Default is 1.
* You can use that property to increase quality of the image, for example for super hight quality exports
* or usage on retina (or similar) displays. pixelRatio will be used to multiply the size of exported image.
* If you export to 500x500 size with pixelRatio = 2, then produced image will have size 1000x1000.
* @param {Boolean} [config.imageSmoothingEnabled] set this to false if you want to disable imageSmoothing
* @returns {String}
*/
toDataURL(
config?: ImageConfig & {
callback?: (url: string) => void;
}
) {
config = config || {};
const mimeType = config.mimeType || null,
quality = config.quality || null;
const url = this._toKonvaCanvas(config).toDataURL(mimeType, quality);
if (config.callback) {
config.callback(url);
}
return url;
}
/**
* converts node into an image. Since the toImage
* method is asynchronous, the resulting image can only be retrieved from the config callback
* or the returned Promise. toImage is most commonly used
* to cache complex drawings as an image so that they don't have to constantly be redrawn
* @method
* @name Konva.Node#toImage
* @param {Object} config
* @param {Function} [config.callback] function executed when the composite has completed
* @param {String} [config.mimeType] can be "image/png" or "image/jpeg".
* "image/png" is the default
* @param {Number} [config.x] x position of canvas section
* @param {Number} [config.y] y position of canvas section
* @param {Number} [config.width] width of canvas section
* @param {Number} [config.height] height of canvas section
* @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType,
* you can specify the quality from 0 to 1, where 0 is very poor quality and 1
* is very high quality
* @param {Number} [config.pixelRatio] pixelRatio of output image. Default is 1.
* You can use that property to increase quality of the image, for example for super hight quality exports
* or usage on retina (or similar) displays. pixelRatio will be used to multiply the size of exported image.
* If you export to 500x500 size with pixelRatio = 2, then produced image will have size 1000x1000.
* @param {Boolean} [config.imageSmoothingEnabled] set this to false if you want to disable imageSmoothing
* @return {Promise}
* @example
* var image = node.toImage({
* callback(img) {
* // do stuff with img
* }
* });
*/
toImage(
config?: ImageConfig & {
callback?: (img: HTMLImageElement) => void;
}
) {
return new Promise((resolve, reject) => {
try {
const callback = config?.callback;
if (callback) delete config.callback;
Util._urlToImage(this.toDataURL(config as any), function (img) {
resolve(img);
callback?.(img);
});
} catch (err) {
reject(err);
}
});
}
/**
* Converts node into a blob. Since the toBlob method is asynchronous,
* the resulting blob can only be retrieved from the config callback
* or the returned Promise.
* @method
* @name Konva.Node#toBlob
* @param {Object} config
* @param {Function} [config.callback] function executed when the composite has completed
* @param {Number} [config.x] x position of canvas section
* @param {Number} [config.y] y position of canvas section
* @param {Number} [config.width] width of canvas section
* @param {Number} [config.height] height of canvas section
* @param {Number} [config.pixelRatio] pixelRatio of output canvas. Default is 1.
* You can use that property to increase quality of the image, for example for super hight quality exports
* or usage on retina (or similar) displays. pixelRatio will be used to multiply the size of exported image.
* If you export to 500x500 size with pixelRatio = 2, then produced image will have size 1000x1000.
* @param {Boolean} [config.imageSmoothingEnabled] set this to false if you want to disable imageSmoothing
* @example
* var blob = await node.toBlob({});
* @returns {Promise}
*/
toBlob(
config?: ImageConfig & {
callback?: (blob: Blob | null) => void;
}
) {
return new Promise((resolve, reject) => {
try {
const callback = config?.callback;
if (callback) delete config.callback;
this.toCanvas(config).toBlob(
(blob) => {
resolve(blob);
callback?.(blob);
},
config?.mimeType,
config?.quality
);
} catch (err) {
reject(err);
}
});
}
setSize(size) {
this.width(size.width);
this.height(size.height);
return this;
}
getSize() {
return {
width: this.width(),
height: this.height(),
};
}
/**
* get class name, which may return Stage, Layer, Group, or shape class names like Rect, Circle, Text, etc.
* @method
* @name Konva.Node#getClassName
* @returns {String}
*/
getClassName() {
return this.className || this.nodeType;
}
/**
* get the node type, which may return Stage, Layer, Group, or Shape
* @method
* @name Konva.Node#getType
* @returns {String}
*/
getType() {
return this.nodeType;
}
getDragDistance(): number {
// compare with undefined because we need to track 0 value
if (this.attrs.dragDistance !== undefined) {
return this.attrs.dragDistance;
} else if (this.parent) {
return this.parent.getDragDistance();
} else {
return Konva.dragDistance;
}
}
_off(type, name?, callback?) {
let evtListeners = this.eventListeners[type],
i,
evtName,
handler;
for (i = 0; i < evtListeners.length; i++) {
evtName = evtListeners[i].name;
handler = evtListeners[i].handler;
// the following two conditions must be true in order to remove a handler:
// 1) the current event name cannot be konva unless the event name is konva
// this enables developers to force remove a konva specific listener for whatever reason
// 2) an event name is not specified, or if one is specified, it matches the current event name
if (
(evtName !== 'konva' || name === 'konva') &&
(!name || evtName === name) &&
(!callback || callback === handler)
) {
evtListeners.splice(i, 1);
if (evtListeners.length === 0) {
delete this.eventListeners[type];
break;
}
i--;
}
}
}
_fireChangeEvent(attr, oldVal, newVal) {
this._fire(attr + CHANGE, {
oldVal: oldVal,
newVal: newVal,
});
}
/**
* add name to node
* @method
* @name Konva.Node#addName
* @param {String} name
* @returns {Konva.Node}
* @example
* node.name('red');
* node.addName('selected');
* node.name(); // return 'red selected'
*/
addName(name: string) {
if (!this.hasName(name)) {
const oldName = this.name();
const newName = oldName ? oldName + ' ' + name : name;
this.name(newName);
}
return this;
}
/**
* check is node has name
* @method
* @name Konva.Node#hasName
* @param {String} name
* @returns {Boolean}
* @example
* node.name('red');
* node.hasName('red'); // return true
* node.hasName('selected'); // return false
* node.hasName(''); // return false
*/
hasName(name) {
if (!name) {
return false;
}
const fullName = this.name();
if (!fullName) {
return false;
}
// if name is '' the "names" will be [''], so I added extra check above
const names = (fullName || '').split(/\s/g);
return names.indexOf(name) !== -1;
}
/**
* remove name from node
* @method
* @name Konva.Node#removeName
* @param {String} name
* @returns {Konva.Node}
* @example
* node.name('red selected');
* node.removeName('selected');
* node.hasName('selected'); // return false
* node.name(); // return 'red'
*/
removeName(name) {
const names = (this.name() || '').split(/\s/g);
const index = names.indexOf(name);
if (index !== -1) {
names.splice(index, 1);
this.name(names.join(' '));
}
return this;
}
/**
* set attr
* @method
* @name Konva.Node#setAttr
* @param {String} attr
* @param {*} val
* @returns {Konva.Node}
* @example
* node.setAttr('x', 5);
*/
setAttr>(
attr: K,
val: K extends keyof AttrConfig ? AttrConfig[K] : any
) {
const func = this[SET + Util._capitalize(attr as string)];
if (Util._isFunction(func)) {
func.call(this, val);
} else {
// otherwise set directly
this._setAttr(attr, val);
}
return this;
}
_requestDraw() {
if (Konva.autoDrawEnabled) {
const drawNode = this.getLayer() || this.getStage();
drawNode?.batchDraw();
}
}
_setAttr>(
key: K,
val: K extends keyof AttrConfig ? AttrConfig[K] : any
) {
const oldVal = this.attrs[key];
if (oldVal === val && !Util.isObject(val)) {
return;
}
if (val === undefined || val === null) {
delete this.attrs[key];
} else {
this.attrs[key] = val;
}
if (this._shouldFireChangeEvents) {
this._fireChangeEvent(key, oldVal, val);
}
this._requestDraw();
}
_setComponentAttr(key, component, val) {
let oldVal;
if (val !== undefined) {
oldVal = this.attrs[key];
if (!oldVal) {
// set value to default value using getAttr
this.attrs[key] = this.getAttr(key);
}
this.attrs[key][component] = val;
this._fireChangeEvent(key, oldVal, val);
}
}
_fireAndBubble(eventType, evt, compareShape?) {
if (evt && this.nodeType === SHAPE) {
evt.target = this;
}
const nonBubbling = [
MOUSEENTER,
MOUSELEAVE,
POINTERENTER,
POINTERLEAVE,
TOUCHENTER,
TOUCHLEAVE,
];
const shouldStop =
nonBubbling.indexOf(eventType) !== -1 &&
((compareShape &&
(this === compareShape ||
(this.isAncestorOf && this.isAncestorOf(compareShape)))) ||
(this.nodeType === 'Stage' && !compareShape));
if (!shouldStop) {
this._fire(eventType, evt);
// simulate event bubbling
const stopBubble =
nonBubbling.indexOf(eventType) !== -1 &&
compareShape &&
compareShape.isAncestorOf &&
compareShape.isAncestorOf(this) &&
!compareShape.isAncestorOf(this.parent);
if (
((evt && !evt.cancelBubble) || !evt) &&
this.parent &&
this.parent.isListening() &&
!stopBubble
) {
if (compareShape && compareShape.parent) {
this._fireAndBubble.call(this.parent, eventType, evt, compareShape);
} else {
this._fireAndBubble.call(this.parent, eventType, evt);
}
}
}
}
static protoListenerMap = new Map();
_getProtoListeners(eventType) {
const { nodeType } = this;
const allListeners = Node.protoListenerMap.get(nodeType) || {};
let events = allListeners?.[eventType];
if (events === undefined) {
//recalculate cache
events = [];
let obj = Object.getPrototypeOf(this);
while (obj) {
const hierarchyEvents = obj.eventListeners?.[eventType] ?? [];
events.push(...hierarchyEvents);
obj = Object.getPrototypeOf(obj);
}
// update cache
allListeners[eventType] = events;
Node.protoListenerMap.set(nodeType, allListeners);
}
return events;
}
_fire(eventType, evt) {
evt = evt || {};
evt.currentTarget = this;
evt.type = eventType;
const topListeners = this._getProtoListeners(eventType);
if (topListeners) {
for (let i = 0; i < topListeners.length; i++) {
topListeners[i].handler.call(this, evt);
}
}
// it is important to iterate over self listeners without cache
// because events can be added/removed while firing
const selfListeners = this.eventListeners[eventType];
if (selfListeners) {
for (let i = 0; i < selfListeners.length; i++) {
selfListeners[i].handler.call(this, evt);
}
}
}
/**
* draw both scene and hit graphs. If the node being drawn is the stage, all of the layers will be cleared and redrawn
* @method
* @name Konva.Node#draw
* @returns {Konva.Node}
*/
draw() {
this.drawScene();
this.drawHit();
return this;
}
// drag & drop
_createDragElement(evt) {
const pointerId = evt ? evt.pointerId : undefined;
const stage = this.getStage();
const ap = this.getAbsolutePosition();
if (!stage) {
return;
}
const pos =
stage._getPointerById(pointerId) ||
stage._changedPointerPositions[0] ||
ap;
DD._dragElements.set(this._id, {
node: this,
startPointerPos: pos,
offset: {
x: pos.x - ap.x,
y: pos.y - ap.y,
},
dragStatus: 'ready',
pointerId,
startEvent: evt,
});
}
/**
* initiate drag and drop.
* @method
* @name Konva.Node#startDrag
*/
startDrag(evt?: any, bubbleEvent = true) {
if (!DD._dragElements.has(this._id)) {
this._createDragElement(evt);
}
const elem = DD._dragElements.get(this._id)!;
elem.dragStatus = 'dragging';
this.fire(
'dragstart',
{
type: 'dragstart',
target: this,
// Use the stored start event if available (from mousedown/touchstart),
// otherwise fall back to the provided event (when startDrag is called programmatically)
evt: (elem.startEvent && elem.startEvent.evt) || (evt && evt.evt),
},
bubbleEvent
);
}
_setDragPosition(evt, elem) {
// const pointers = this.getStage().getPointersPositions();
// const pos = pointers.find(p => p.id === this._dragEventId);
const pos = this.getStage()!._getPointerById(elem.pointerId);
if (!pos) {
return;
}
let newNodePos = {
x: pos.x - elem.offset.x,
y: pos.y - elem.offset.y,
};
const dbf = this.dragBoundFunc();
if (dbf !== undefined) {
const bounded = dbf.call(this, newNodePos, evt);
if (!bounded) {
Util.warn(
'dragBoundFunc did not return any value. That is unexpected behavior. You must return new absolute position from dragBoundFunc.'
);
} else {
newNodePos = bounded;
}
}
if (
!this._lastPos ||
this._lastPos.x !== newNodePos.x ||
this._lastPos.y !== newNodePos.y
) {
this.setAbsolutePosition(newNodePos);
this._requestDraw();
}
this._lastPos = newNodePos;
}
/**
* stop drag and drop
* @method
* @name Konva.Node#stopDrag
*/
stopDrag(evt?) {
const elem = DD._dragElements.get(this._id);
if (elem) {
elem.dragStatus = 'stopped';
}
DD._endDragBefore(evt);
DD._endDragAfter(evt);
}
setDraggable(draggable) {
this._setAttr('draggable', draggable);
this._dragChange();
}
/**
* determine if node is currently in drag and drop mode
* @method
* @name Konva.Node#isDragging
*/
isDragging() {
const elem = DD._dragElements.get(this._id);
return elem ? elem.dragStatus === 'dragging' : false;
}
_listenDrag() {
this._dragCleanup();
this.on('mousedown.konva touchstart.konva', function (evt) {
const shouldCheckButton = evt.evt['button'] !== undefined;
const canDrag =
!shouldCheckButton || Konva.dragButtons.indexOf(evt.evt['button']) >= 0;
if (!canDrag) {
return;
}
if (this.isDragging()) {
return;
}
let hasDraggingChild = false;
DD._dragElements.forEach((elem) => {
if (this.isAncestorOf(elem.node)) {
hasDraggingChild = true;
}
});
// nested drag can be started
// in that case we don't need to start new drag
if (!hasDraggingChild) {
this._createDragElement(evt);
}
});
}
_dragChange() {
if (this.attrs.draggable) {
this._listenDrag();
} else {
// remove event listeners
this._dragCleanup();
/*
* force drag and drop to end
* if this node is currently in
* drag and drop mode
*/
const stage = this.getStage();
if (!stage) {
return;
}
const dragElement = DD._dragElements.get(this._id);
const isDragging = dragElement && dragElement.dragStatus === 'dragging';
const isReady = dragElement && dragElement.dragStatus === 'ready';
if (isDragging) {
this.stopDrag();
} else if (isReady) {
DD._dragElements.delete(this._id);
}
}
}
_dragCleanup() {
this.off('mousedown.konva');
this.off('touchstart.konva');
}
/**
* determine if node (at least partially) is currently in user-visible area
* @method
* @param {(Number | Object)} margin optional margin in pixels
* @param {Number} margin.x
* @param {Number} margin.y
* @returns {Boolean}
* @name Konva.Node#isClientRectOnScreen
* @example
* // get index
* // default calculations
* var isOnScreen = node.isClientRectOnScreen()
* // increase object size (or screen size) for cases when objects close to the screen still need to be marked as "visible"
* var isOnScreen = node.isClientRectOnScreen({ x: stage.width(), y: stage.height() })
*/
isClientRectOnScreen(
margin: { x: number; y: number } = { x: 0, y: 0 }
): boolean {
const stage = this.getStage();
if (!stage) {
return false;
}
const screenRect = {
x: -margin.x,
y: -margin.y,
width: stage.width() + 2 * margin.x,
height: stage.height() + 2 * margin.y,
};
return Util.haveIntersection(screenRect, this.getClientRect());
}
// @ts-ignore:
preventDefault: GetSet;
// from filters
blue: GetSet;
brightness: GetSet;
contrast: GetSet;
blurRadius: GetSet;
luminance: GetSet;
green: GetSet;
alpha: GetSet;
hue: GetSet;
kaleidoscopeAngle: GetSet;
kaleidoscopePower: GetSet;
levels: GetSet;
noise: GetSet;
pixelSize: GetSet;
red: GetSet;
saturation: GetSet;
threshold: GetSet;
value: GetSet;
dragBoundFunc: GetSet<
(this: Node, pos: Vector2d, event: any) => Vector2d,
this
>;
draggable: GetSet;
dragDistance: GetSet;
embossBlend: GetSet;
embossDirection: GetSet;
embossStrength: GetSet;
embossWhiteLevel: GetSet;
enhance: GetSet;
filters: GetSet;
position: GetSet;
absolutePosition: GetSet;
size: GetSet<{ width: number; height: number }, this>;
id: GetSet;
listening: GetSet;
name: GetSet;
offset: GetSet;
offsetX: GetSet;
offsetY: GetSet;
opacity: GetSet;
rotation: GetSet;
zIndex: GetSet;
scale: GetSet;
scaleX: GetSet;
scaleY: GetSet;
skew: GetSet;
skewX: GetSet;
skewY: GetSet;
to: (params: AnimTo) => void;
transformsEnabled: GetSet;
visible: GetSet;
width: GetSet;
height: GetSet;
x: GetSet;
y: GetSet;
globalCompositeOperation: GetSet;
/**
* create node with JSON string or an Object. De-serializtion does not generate custom
* shape drawing functions, images, or event handlers (this would make the
* serialized object huge). If your app uses custom shapes, images, and
* event handlers (it probably does), then you need to select the appropriate
* shapes after loading the stage and set these properties via on(), setSceneFunc(),
* and setImage() methods
* @method
* @memberof Konva.Node
* @param {String|Object} json string or object
* @param {Element} [container] optional container dom element used only if you're
* creating a stage node
*/
static create(data, container?) {
if (Util._isString(data)) {
data = JSON.parse(data);
}
return this._createNode(data, container);
}
static _createNode(obj, container?) {
let className = Node.prototype.getClassName.call(obj),
children = obj.children,
no,
len,
n;
// if container was passed in, add it to attrs
if (container) {
obj.attrs.container = container;
}
if (!Konva[className]) {
Util.warn(
'Can not find a node with class name "' +
className +
'". Fallback to "Shape".'
);
className = 'Shape';
}
const Class = Konva[className];
no = new Class(obj.attrs);
if (children) {
len = children.length;
for (n = 0; n < len; n++) {
no.add(Node._createNode(children[n]));
}
}
return no;
}
}
interface AnimTo extends NodeConfig {
onFinish?: Function;
onUpdate?: Function;
duration?: number;
}
Node.prototype.nodeType = 'Node';
Node.prototype._attrsAffectingSize = [];
// attache events listeners once into prototype
// that way we don't spend too much time on making an new instance
Node.prototype.eventListeners = {};
Node.prototype.on(TRANSFORM_CHANGE_STR, function () {
if (this._batchingTransformChange) {
this._needClearTransformCache = true;
return;
}
this._clearCache(TRANSFORM);
this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM);
});
Node.prototype.on('visibleChange.konva', function () {
this._clearSelfAndDescendantCache(VISIBLE);
});
Node.prototype.on('listeningChange.konva', function () {
this._clearSelfAndDescendantCache(LISTENING);
});
Node.prototype.on('opacityChange.konva', function () {
this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY);
});
const addGetterSetter = Factory.addGetterSetter;
/**
* get/set zIndex relative to the node's siblings who share the same parent.
* Please remember that zIndex is not absolute (like in CSS). It is relative to parent element only.
* @name Konva.Node#zIndex
* @method
* @param {Number} index
* @returns {Number}
* @example
* // get index
* var index = node.zIndex();
*
* // set index
* node.zIndex(2);
*/
addGetterSetter(Node, 'zIndex');
/**
* get/set node absolute position
* @name Konva.Node#absolutePosition
* @method
* @param {Object} pos
* @param {Number} pos.x
* @param {Number} pos.y
* @returns {Object}
* @example
* // get position
* var position = node.absolutePosition();
*
* // set position
* node.absolutePosition({
* x: 5,
* y: 10
* });
*/
addGetterSetter(Node, 'absolutePosition');
addGetterSetter(Node, 'position');
/**
* get/set node position relative to parent
* @name Konva.Node#position
* @method
* @param {Object} pos
* @param {Number} pos.x
* @param {Number} pos.y
* @returns {Object}
* @example
* // get position
* var position = node.position();
*
* // set position
* node.position({
* x: 5,
* y: 10
* });
*/
addGetterSetter(Node, 'x', 0, getNumberValidator());
/**
* get/set x position
* @name Konva.Node#x
* @method
* @param {Number} x
* @returns {Object}
* @example
* // get x
* var x = node.x();
*
* // set x
* node.x(5);
*/
addGetterSetter(Node, 'y', 0, getNumberValidator());
/**
* get/set y position
* @name Konva.Node#y
* @method
* @param {Number} y
* @returns {Integer}
* @example
* // get y
* var y = node.y();
*
* // set y
* node.y(5);
*/
addGetterSetter(
Node,
'globalCompositeOperation',
'source-over',
getStringValidator()
);
/**
* get/set globalCompositeOperation of a node. globalCompositeOperation DOESN'T affect hit graph of nodes. So they are still trigger to events as they have default "source-over" globalCompositeOperation.
* @name Konva.Node#globalCompositeOperation
* @method
* @param {String} type
* @returns {String}
* @example
* // get globalCompositeOperation
* var globalCompositeOperation = shape.globalCompositeOperation();
*
* // set globalCompositeOperation
* shape.globalCompositeOperation('source-in');
*/
addGetterSetter(Node, 'opacity', 1, getNumberValidator());
/**
* get/set opacity. Opacity values range from 0 to 1.
* A node with an opacity of 0 is fully transparent, and a node
* with an opacity of 1 is fully opaque
* @name Konva.Node#opacity
* @method
* @param {Object} opacity
* @returns {Number}
* @example
* // get opacity
* var opacity = node.opacity();
*
* // set opacity
* node.opacity(0.5);
*/
addGetterSetter(Node, 'name', '', getStringValidator());
/**
* get/set name.
* @name Konva.Node#name
* @method
* @param {String} name
* @returns {String}
* @example
* // get name
* var name = node.name();
*
* // set name
* node.name('foo');
*
* // also node may have multiple names (as css classes)
* node.name('foo bar');
*/
addGetterSetter(Node, 'id', '', getStringValidator());
/**
* get/set id. Id is global for whole page.
* @name Konva.Node#id
* @method
* @param {String} id
* @returns {String}
* @example
* // get id
* var name = node.id();
*
* // set id
* node.id('foo');
*/
addGetterSetter(Node, 'rotation', 0, getNumberValidator());
/**
* get/set rotation in degrees
* @name Konva.Node#rotation
* @method
* @param {Number} rotation
* @returns {Number}
* @example
* // get rotation in degrees
* var rotation = node.rotation();
*
* // set rotation in degrees
* node.rotation(45);
*/
Factory.addComponentsGetterSetter(Node, 'scale', ['x', 'y']);
/**
* get/set scale
* @name Konva.Node#scale
* @param {Object} scale
* @param {Number} scale.x
* @param {Number} scale.y
* @method
* @returns {Object}
* @example
* // get scale
* var scale = node.scale();
*
* // set scale
* shape.scale({
* x: 2,
* y: 3
* });
*/
addGetterSetter(Node, 'scaleX', 1, getNumberValidator());
/**
* get/set scale x
* @name Konva.Node#scaleX
* @param {Number} x
* @method
* @returns {Number}
* @example
* // get scale x
* var scaleX = node.scaleX();
*
* // set scale x
* node.scaleX(2);
*/
addGetterSetter(Node, 'scaleY', 1, getNumberValidator());
/**
* get/set scale y
* @name Konva.Node#scaleY
* @param {Number} y
* @method
* @returns {Number}
* @example
* // get scale y
* var scaleY = node.scaleY();
*
* // set scale y
* node.scaleY(2);
*/
Factory.addComponentsGetterSetter(Node, 'skew', ['x', 'y']);
/**
* get/set skew
* @name Konva.Node#skew
* @param {Object} skew
* @param {Number} skew.x
* @param {Number} skew.y
* @method
* @returns {Object}
* @example
* // get skew
* var skew = node.skew();
*
* // set skew
* node.skew({
* x: 20,
* y: 10
* });
*/
addGetterSetter(Node, 'skewX', 0, getNumberValidator());
/**
* get/set skew x
* @name Konva.Node#skewX
* @param {Number} x
* @method
* @returns {Number}
* @example
* // get skew x
* var skewX = node.skewX();
*
* // set skew x
* node.skewX(3);
*/
addGetterSetter(Node, 'skewY', 0, getNumberValidator());
/**
* get/set skew y
* @name Konva.Node#skewY
* @param {Number} y
* @method
* @returns {Number}
* @example
* // get skew y
* var skewY = node.skewY();
*
* // set skew y
* node.skewY(3);
*/
Factory.addComponentsGetterSetter(Node, 'offset', ['x', 'y']);
/**
* get/set offset. Offsets the default position and rotation point
* @method
* @param {Object} offset
* @param {Number} offset.x
* @param {Number} offset.y
* @returns {Object}
* @example
* // get offset
* var offset = node.offset();
*
* // set offset
* node.offset({
* x: 20,
* y: 10
* });
*/
addGetterSetter(Node, 'offsetX', 0, getNumberValidator());
/**
* get/set offset x
* @name Konva.Node#offsetX
* @method
* @param {Number} x
* @returns {Number}
* @example
* // get offset x
* var offsetX = node.offsetX();
*
* // set offset x
* node.offsetX(3);
*/
addGetterSetter(Node, 'offsetY', 0, getNumberValidator());
/**
* get/set offset y
* @name Konva.Node#offsetY
* @method
* @param {Number} y
* @returns {Number}
* @example
* // get offset y
* var offsetY = node.offsetY();
*
* // set offset y
* node.offsetY(3);
*/
addGetterSetter(Node, 'dragDistance', undefined, getNumberValidator());
/**
* get/set drag distance
* @name Konva.Node#dragDistance
* @method
* @param {Number} distance
* @returns {Number}
* @example
* // get drag distance
* var dragDistance = node.dragDistance();
*
* // set distance
* // node starts dragging only if pointer moved more then 3 pixels
* node.dragDistance(3);
* // or set globally
* Konva.dragDistance = 3;
*/
addGetterSetter(Node, 'width', 0, getNumberValidator());
/**
* get/set width
* @name Konva.Node#width
* @method
* @param {Number} width
* @returns {Number}
* @example
* // get width
* var width = node.width();
*
* // set width
* node.width(100);
*/
addGetterSetter(Node, 'height', 0, getNumberValidator());
/**
* get/set height
* @name Konva.Node#height
* @method
* @param {Number} height
* @returns {Number}
* @example
* // get height
* var height = node.height();
*
* // set height
* node.height(100);
*/
addGetterSetter(Node, 'listening', true, getBooleanValidator());
/**
* get/set listening attr. If you need to determine if a node is listening or not
* by taking into account its parents, use the isListening() method
* nodes with listening set to false will not be detected in hit graph
* so they will be ignored in container.getIntersection() method
* @name Konva.Node#listening
* @method
* @param {Boolean} listening Can be true, or false. The default is true.
* @returns {Boolean}
* @example
* // get listening attr
* var listening = node.listening();
*
* // stop listening for events, remove node and all its children from hit graph
* node.listening(false);
*
* // listen to events according to the parent
* node.listening(true);
*/
/**
* get/set preventDefault
* By default all shapes will prevent default behavior
* of a browser on a pointer move or tap.
* that will prevent native scrolling when you are trying to drag&drop a node
* but sometimes you may need to enable default actions
* in that case you can set the property to false
* @name Konva.Node#preventDefault
* @method
* @param {Boolean} preventDefault
* @returns {Boolean}
* @example
* // get preventDefault
* var shouldPrevent = shape.preventDefault();
*
* // set preventDefault
* shape.preventDefault(false);
*/
addGetterSetter(Node, 'preventDefault', true, getBooleanValidator());
addGetterSetter(Node, 'filters', undefined, function (this: Node, val) {
this._filterUpToDate = false;
return val;
});
/**
* get/set filters. Supports function filters, CSS filter strings, or mixed arrays.
* CSS filters are applied using native browser performance when possible, function filters use ImageData manipulation.
* CSS filters automatically fall back to function filters in unsupported browsers.
* @name Konva.Node#filters
* @method
* @param {Array} filters array of filter functions and/or CSS filter strings
* @returns {Array}
* @example
* // get filters
* var filters = node.filters();
*
* // set CSS filters only (no caching required, uses native performance)
* node.filters(['blur(5px)', 'brightness(1.2)', 'contrast(1.5)']);
*
* // set function filters only (caching required)
* node.cache();
* node.filters([Konva.Filters.Blur, Konva.Filters.Sepia, Konva.Filters.Invert]);
*
* // mix CSS and function filters (caching required, uses fallback strategy)
* node.cache();
* node.filters([
* 'blur(3px)', // CSS filter
* Konva.Filters.Invert, // function filter
* 'brightness(1.5)' // CSS filter
* ]);
*/
addGetterSetter(Node, 'visible', true, getBooleanValidator());
/**
* get/set visible attr. Can be true, or false. The default is true.
* If you need to determine if a node is visible or not
* by taking into account its parents, use the isVisible() method
* @name Konva.Node#visible
* @method
* @param {Boolean} visible
* @returns {Boolean}
* @example
* // get visible attr
* var visible = node.visible();
*
* // make invisible
* node.visible(false);
*
* // make visible (according to the parent)
* node.visible(true);
*
*/
addGetterSetter(Node, 'transformsEnabled', 'all', getStringValidator());
/**
* get/set transforms that are enabled. Can be "all", "none", or "position". The default
* is "all"
* @name Konva.Node#transformsEnabled
* @method
* @param {String} enabled
* @returns {String}
* @example
* // enable position transform only to improve draw performance
* node.transformsEnabled('position');
*
* // enable all transforms
* node.transformsEnabled('all');
*/
/**
* get/set node size
* @name Konva.Node#size
* @method
* @param {Object} size
* @param {Number} size.width
* @param {Number} size.height
* @returns {Object}
* @example
* // get node size
* var size = node.size();
* var width = size.width;
* var height = size.height;
*
* // set size
* node.size({
* width: 100,
* height: 200
* });
*/
addGetterSetter(Node, 'size');
/**
* get/set drag bound function. This is used to override the default
* drag and drop position.
* @name Konva.Node#dragBoundFunc
* @method
* @param {Function} dragBoundFunc
* @returns {Function}
* @example
* // get drag bound function
* var dragBoundFunc = node.dragBoundFunc();
*
* // create vertical drag and drop
* node.dragBoundFunc(function(pos){
* // important pos - is absolute position of the node
* // you should return absolute position too
* return {
* x: this.absolutePosition().x,
* y: pos.y
* };
* });
*/
addGetterSetter(Node, 'dragBoundFunc');
/**
* get/set draggable flag
* @name Konva.Node#draggable
* @method
* @param {Boolean} draggable
* @returns {Boolean}
* @example
* // get draggable flag
* var draggable = node.draggable();
*
* // enable drag and drop
* node.draggable(true);
*
* // disable drag and drop
* node.draggable(false);
*/
addGetterSetter(Node, 'draggable', false, getBooleanValidator());
Factory.backCompat(Node, {
rotateDeg: 'rotate',
setRotationDeg: 'setRotation',
getRotationDeg: 'getRotation',
});
================================================
FILE: src/PointerEvents.ts
================================================
import type { KonvaEventObject } from './Node.ts';
import { Konva } from './Global.ts';
import type { Shape } from './Shape.ts';
import type { Stage } from './Stage.ts';
const Captures = new Map();
// we may use this module for capturing touch events too
// so make sure we don't do something super specific to pointer
const SUPPORT_POINTER_EVENTS = Konva._global['PointerEvent'] !== undefined;
export interface KonvaPointerEvent extends KonvaEventObject {
pointerId: number;
}
export function getCapturedShape(pointerId: number) {
return Captures.get(pointerId);
}
export function createEvent(evt: PointerEvent): KonvaPointerEvent {
return {
evt,
pointerId: evt.pointerId,
} as any;
}
export function hasPointerCapture(pointerId: number, shape: Shape | Stage) {
return Captures.get(pointerId) === shape;
}
export function setPointerCapture(pointerId: number, shape: Shape | Stage) {
releaseCapture(pointerId);
const stage = shape.getStage();
if (!stage) return;
Captures.set(pointerId, shape);
if (SUPPORT_POINTER_EVENTS) {
shape._fire(
'gotpointercapture',
createEvent(new PointerEvent('gotpointercapture'))
);
}
}
export function releaseCapture(pointerId: number, target?: Shape | Stage) {
const shape = Captures.get(pointerId);
if (!shape) return;
const stage = shape.getStage();
if (stage && stage.content) {
// stage.content.releasePointerCapture(pointerId);
}
Captures.delete(pointerId);
if (SUPPORT_POINTER_EVENTS) {
shape._fire(
'lostpointercapture',
createEvent(new PointerEvent('lostpointercapture'))
);
}
}
================================================
FILE: src/Shape.ts
================================================
import { Konva } from './Global.ts';
import { Transform, Util } from './Util.ts';
import { Factory } from './Factory.ts';
import type { NodeConfig } from './Node.ts';
import { Node } from './Node.ts';
import {
getNumberValidator,
getNumberOrAutoValidator,
getStringValidator,
getBooleanValidator,
getStringOrGradientValidator,
} from './Validators.ts';
import type { Context, SceneContext } from './Context.ts';
import { _registerNode } from './Global.ts';
import * as PointerEvents from './PointerEvents.ts';
import type { GetSet, Vector2d } from './types.ts';
import type { HitCanvas, SceneCanvas } from './Canvas.ts';
// hack from here https://stackoverflow.com/questions/52667959/what-is-the-purpose-of-bivariancehack-in-typescript-types/52668133#52668133
export type ShapeConfigHandler = {
bivarianceHack(ctx: Context, shape: TTarget): void;
}['bivarianceHack'];
export type LineJoin = 'round' | 'bevel' | 'miter';
export type LineCap = 'butt' | 'round' | 'square';
export type ShapeConfig = NodeConfig & {
fill?: string | CanvasGradient;
fillPatternImage?: HTMLImageElement;
fillPatternX?: number;
fillPatternY?: number;
fillPatternOffset?: Vector2d;
fillPatternOffsetX?: number;
fillPatternOffsetY?: number;
fillPatternScale?: Vector2d;
fillPatternScaleX?: number;
fillPatternScaleY?: number;
fillPatternRotation?: number;
fillPatternRepeat?: string;
fillLinearGradientStartPoint?: Vector2d;
fillLinearGradientStartPointX?: number;
fillLinearGradientStartPointY?: number;
fillLinearGradientEndPoint?: Vector2d;
fillLinearGradientEndPointX?: number;
fillLinearGradientEndPointY?: number;
fillLinearGradientColorStops?: Array;
fillRadialGradientStartPoint?: Vector2d;
fillRadialGradientStartPointX?: number;
fillRadialGradientStartPointY?: number;
fillRadialGradientEndPoint?: Vector2d;
fillRadialGradientEndPointX?: number;
fillRadialGradientEndPointY?: number;
fillRadialGradientStartRadius?: number;
fillRadialGradientEndRadius?: number;
fillRadialGradientColorStops?: Array;
fillEnabled?: boolean;
fillPriority?: string;
fillRule?: CanvasFillRule;
stroke?: string | CanvasGradient;
strokeWidth?: number;
fillAfterStrokeEnabled?: boolean;
hitStrokeWidth?: number | string;
strokeScaleEnabled?: boolean;
strokeHitEnabled?: boolean;
strokeEnabled?: boolean;
lineJoin?: LineJoin;
lineCap?: LineCap;
miterLimit?: number;
sceneFunc?: (con: Context, shape: Shape) => void;
hitFunc?: (con: Context, shape: Shape) => void;
shadowColor?: string;
shadowBlur?: number;
shadowOffset?: Vector2d;
shadowOffsetX?: number;
shadowOffsetY?: number;
shadowOpacity?: number;
shadowEnabled?: boolean;
shadowForStrokeEnabled?: boolean;
dash?: number[];
dashOffset?: number;
dashEnabled?: boolean;
perfectDrawEnabled?: boolean;
};
export interface ShapeGetClientRectConfig {
skipTransform?: boolean;
skipShadow?: boolean;
skipStroke?: boolean;
relativeTo?: Node;
}
export type FillFuncOutput =
| void
| [Path2D | CanvasFillRule]
| [Path2D, CanvasFillRule];
const HAS_SHADOW = 'hasShadow';
const SHADOW_RGBA = 'shadowRGBA';
const patternImage = 'patternImage';
const linearGradient = 'linearGradient';
const radialGradient = 'radialGradient';
let dummyContext: CanvasRenderingContext2D;
function getDummyContext(): CanvasRenderingContext2D {
if (dummyContext) {
return dummyContext;
}
dummyContext = Util.createCanvasElement().getContext('2d')!;
return dummyContext;
}
export const shapes: { [key: string]: Shape } = {};
// TODO: idea - use only "remove" (or destroy method)
// how? on add, check that every inner shape has reference in konva store with color
// on remove - clear that reference
// the approach is good. But what if we want to cache the shape before we add it into the stage
// what color to use for hit test?
function _fillFunc(this: Node, context) {
const fillRule = this.attrs.fillRule;
if (fillRule) {
context.fill(fillRule);
} else {
context.fill();
}
}
function _strokeFunc(context) {
context.stroke();
}
function _fillFuncHit(this: Node, context) {
const fillRule = this.attrs.fillRule;
if (fillRule) {
context.fill(fillRule);
} else {
context.fill();
}
}
function _strokeFuncHit(context) {
context.stroke();
}
function _clearHasShadowCache(this: Node) {
this._clearCache(HAS_SHADOW);
}
function _clearGetShadowRGBACache(this: Node) {
this._clearCache(SHADOW_RGBA);
}
function _clearFillPatternCache(this: Node) {
this._clearCache(patternImage);
}
function _clearLinearGradientCache(this: Node) {
this._clearCache(linearGradient);
}
function _clearRadialGradientCache(this: Node) {
this._clearCache(radialGradient);
}
/**
* Shape constructor. Shapes are primitive objects such as rectangles,
* circles, text, lines, etc.
* @constructor
* @memberof Konva
* @augments Konva.Node
* @param {Object} config
* @@shapeParams
* @@nodeParams
* @example
* var customShape = new Konva.Shape({
* x: 5,
* y: 10,
* fill: 'red',
* // a Konva.Canvas renderer is passed into the sceneFunc function
* sceneFunc (context, shape) {
* context.beginPath();
* context.moveTo(200, 50);
* context.lineTo(420, 80);
* context.quadraticCurveTo(300, 100, 260, 170);
* context.closePath();
* // Konva specific method
* context.fillStrokeShape(shape);
* }
*});
*/
export class Shape<
Config extends ShapeConfig = ShapeConfig,
> extends Node {
_centroid: boolean;
colorKey: string;
_fillFunc: (ctx: Context) => FillFuncOutput;
_strokeFunc: (ctx: Context) => void;
_fillFuncHit: (ctx: Context) => void;
_strokeFuncHit: (ctx: Context) => void;
constructor(config?: Config) {
super(config);
// set colorKey
let key: string;
let attempts = 0;
while (true) {
key = Util.getHitColor();
if (key && !(key in shapes)) {
break;
}
attempts++;
if (attempts >= 10000) {
Util.warn(
'Failed to find a unique color key for a shape. Konva may work incorrectly. Most likely your browser is using canvas farbling. Consider disabling it.'
);
key = Util.getRandomColor();
break;
}
}
this.colorKey = key;
shapes[key] = this;
}
/**
* @deprecated
*/
getContext() {
Util.warn('shape.getContext() method is deprecated. Please do not use it.');
return this.getLayer()!.getContext();
}
/**
* @deprecated
*/
getCanvas() {
Util.warn('shape.getCanvas() method is deprecated. Please do not use it.');
return this.getLayer()!.getCanvas();
}
getSceneFunc() {
return this.attrs.sceneFunc || this['_sceneFunc'];
}
getHitFunc() {
return this.attrs.hitFunc || this['_hitFunc'];
}
/**
* returns whether or not a shadow will be rendered
* @method
* @name Konva.Shape#hasShadow
* @returns {Boolean}
*/
hasShadow() {
return this._getCache(HAS_SHADOW, this._hasShadow);
}
_hasShadow() {
return (
this.shadowEnabled() &&
this.shadowOpacity() !== 0 &&
!!(
this.shadowColor() ||
this.shadowBlur() ||
this.shadowOffsetX() ||
this.shadowOffsetY()
)
);
}
_getFillPattern() {
return this._getCache(patternImage, this.__getFillPattern);
}
__getFillPattern() {
if (this.fillPatternImage()) {
const ctx = getDummyContext();
const pattern = ctx.createPattern(
this.fillPatternImage(),
this.fillPatternRepeat() || 'repeat'
);
if (pattern && pattern.setTransform) {
const tr = new Transform();
tr.translate(this.fillPatternX(), this.fillPatternY());
tr.rotate(Konva.getAngle(this.fillPatternRotation()));
tr.scale(this.fillPatternScaleX(), this.fillPatternScaleY());
tr.translate(
-1 * this.fillPatternOffsetX(),
-1 * this.fillPatternOffsetY()
);
const m = tr.getMatrix();
const matrix =
typeof DOMMatrix === 'undefined'
? {
a: m[0], // Horizontal scaling. A value of 1 results in no scaling.
b: m[1], // Vertical skewing.
c: m[2], // Horizontal skewing.
d: m[3],
e: m[4], // Horizontal translation (moving).
f: m[5], // Vertical translation (moving).
}
: new DOMMatrix(m);
pattern.setTransform(matrix);
}
return pattern;
}
}
_getLinearGradient() {
return this._getCache(linearGradient, this.__getLinearGradient);
}
__getLinearGradient() {
const colorStops = this.fillLinearGradientColorStops();
if (colorStops) {
const ctx = getDummyContext();
const start = this.fillLinearGradientStartPoint();
const end = this.fillLinearGradientEndPoint();
const grd = ctx.createLinearGradient(start.x, start.y, end.x, end.y);
// build color stops
for (let n = 0; n < colorStops.length; n += 2) {
grd.addColorStop(colorStops[n] as number, colorStops[n + 1] as string);
}
return grd;
}
}
_getRadialGradient() {
return this._getCache(radialGradient, this.__getRadialGradient);
}
__getRadialGradient() {
const colorStops = this.fillRadialGradientColorStops();
if (colorStops) {
const ctx = getDummyContext();
const start = this.fillRadialGradientStartPoint();
const end = this.fillRadialGradientEndPoint();
const grd = ctx.createRadialGradient(
start.x,
start.y,
this.fillRadialGradientStartRadius(),
end.x,
end.y,
this.fillRadialGradientEndRadius()
);
// build color stops
for (let n = 0; n < colorStops.length; n += 2) {
grd.addColorStop(colorStops[n] as number, colorStops[n + 1] as string);
}
return grd;
}
}
getShadowRGBA() {
return this._getCache(SHADOW_RGBA, this._getShadowRGBA);
}
_getShadowRGBA() {
if (!this.hasShadow()) {
return;
}
const rgba = Util.colorToRGBA(this.shadowColor());
if (rgba) {
return (
'rgba(' +
rgba.r +
',' +
rgba.g +
',' +
rgba.b +
',' +
rgba.a * (this.shadowOpacity() || 1) +
')'
);
}
}
/**
* returns whether or not the shape will be filled
* @method
* @name Konva.Shape#hasFill
* @returns {Boolean}
*/
hasFill() {
return this._calculate(
'hasFill',
[
'fillEnabled',
'fill',
'fillPatternImage',
'fillLinearGradientColorStops',
'fillRadialGradientColorStops',
],
() => {
return (
this.fillEnabled() &&
!!(
this.fill() ||
this.fillPatternImage() ||
this.fillLinearGradientColorStops() ||
this.fillRadialGradientColorStops()
)
);
}
);
}
/**
* returns whether or not the shape will be stroked
* @method
* @name Konva.Shape#hasStroke
* @returns {Boolean}
*/
hasStroke() {
return this._calculate(
'hasStroke',
[
'strokeEnabled',
'strokeWidth',
'stroke',
'strokeLinearGradientColorStops',
],
() => {
return (
this.strokeEnabled() &&
this.strokeWidth() &&
!!(this.stroke() || this.strokeLinearGradientColorStops())
// this.getStrokeRadialGradientColorStops()
);
}
);
// return (
// this.strokeEnabled() &&
// this.strokeWidth() &&
// !!(this.stroke() || this.strokeLinearGradientColorStops())
// // this.getStrokeRadialGradientColorStops()
// );
}
hasHitStroke() {
const width = this.hitStrokeWidth();
// on auto just check by stroke
if (width === 'auto') {
return this.hasStroke();
}
// we should enable hit stroke if stroke is enabled
// and we have some value from width
return this.strokeEnabled() && !!width;
}
/**
* determines if point is in the shape, regardless if other shapes are on top of it. Note: because
* this method clears a temporary canvas and then redraws the shape, it performs very poorly if executed many times
* consecutively. Please use the {@link Konva.Stage#getIntersection} method if at all possible
* because it performs much better
* @method
* @name Konva.Shape#intersects
* @param {Object} point
* @param {Number} point.x
* @param {Number} point.y
* @returns {Boolean}
*/
intersects(point: Vector2d) {
const stage = this.getStage();
if (!stage) {
return false;
}
const bufferHitCanvas = stage.bufferHitCanvas;
bufferHitCanvas.getContext().clear();
this.drawHit(bufferHitCanvas, undefined, true);
const p = bufferHitCanvas.context.getImageData(
Math.round(point.x),
Math.round(point.y),
1,
1
).data;
return p[3] > 0;
}
destroy() {
Node.prototype.destroy.call(this);
delete shapes[this.colorKey];
delete (this as any).colorKey;
return this;
}
// why do we need buffer canvas?
// it give better result when a shape has
// stroke with fill and with some opacity
_useBufferCanvas(forceFill?: boolean): boolean {
// image and sprite still has "fill" as image
// so they use that method with forced fill
// it probably will be simpler, then copy/paste the code
// force skip buffer canvas
const perfectDrawEnabled = this.attrs.perfectDrawEnabled ?? true;
if (!perfectDrawEnabled) {
return false;
}
const hasFill = forceFill || this.hasFill();
const hasStroke = this.hasStroke();
const isTransparent = this.getAbsoluteOpacity() !== 1;
if (hasFill && hasStroke && isTransparent) {
return true;
}
const hasShadow = this.hasShadow();
const strokeForShadow = this.shadowForStrokeEnabled();
if (hasFill && hasStroke && hasShadow && strokeForShadow) {
return true;
}
return false;
}
setStrokeHitEnabled(val: number) {
Util.warn(
'strokeHitEnabled property is deprecated. Please use hitStrokeWidth instead.'
);
if (val) {
this.hitStrokeWidth('auto');
} else {
this.hitStrokeWidth(0);
}
}
getStrokeHitEnabled() {
if (this.hitStrokeWidth() === 0) {
return false;
} else {
return true;
}
}
/**
* return self rectangle (x, y, width, height) of shape.
* This method are not taken into account transformation and styles.
* @method
* @name Konva.Shape#getSelfRect
* @returns {Object} rect with {x, y, width, height} properties
* @example
*
* rect.getSelfRect(); // return {x:0, y:0, width:rect.width(), height:rect.height()}
* circle.getSelfRect(); // return {x: - circle.width() / 2, y: - circle.height() / 2, width:circle.width(), height:circle.height()}
*
*/
getSelfRect() {
const size = this.size();
return {
x: this._centroid ? -size.width / 2 : 0,
y: this._centroid ? -size.height / 2 : 0,
width: size.width,
height: size.height,
};
}
getClientRect(config: ShapeGetClientRectConfig = {}) {
// if we have a cached parent, it will use cached transform matrix
// but we don't want to that
let hasCachedParent = false;
let parent = this.getParent();
while (parent) {
if (parent.isCached()) {
hasCachedParent = true;
break;
}
parent = parent.getParent();
}
const skipTransform = config.skipTransform;
// force relative to stage if we have a cached parent
const relativeTo =
config.relativeTo || (hasCachedParent && this.getStage()) || undefined;
const fillRect = this.getSelfRect();
const applyStroke = !config.skipStroke && this.hasStroke();
const strokeWidth: number = (applyStroke && this.strokeWidth()) || 0;
const fillAndStrokeWidth = fillRect.width + strokeWidth;
const fillAndStrokeHeight = fillRect.height + strokeWidth;
const applyShadow = !config.skipShadow && this.hasShadow();
const shadowOffsetX = applyShadow ? this.shadowOffsetX() : 0;
const shadowOffsetY = applyShadow ? this.shadowOffsetY() : 0;
const preWidth = fillAndStrokeWidth + Math.abs(shadowOffsetX);
const preHeight = fillAndStrokeHeight + Math.abs(shadowOffsetY);
const blurRadius = (applyShadow && this.shadowBlur()) || 0;
const width = preWidth + blurRadius * 2;
const height = preHeight + blurRadius * 2;
const rect = {
width: width,
height: height,
x:
-(strokeWidth / 2 + blurRadius) +
Math.min(shadowOffsetX, 0) +
fillRect.x,
y:
-(strokeWidth / 2 + blurRadius) +
Math.min(shadowOffsetY, 0) +
fillRect.y,
};
if (!skipTransform) {
return this._transformedRect(rect, relativeTo);
}
return rect;
}
drawScene(can?: SceneCanvas, top?: Node, bufferCanvas?: SceneCanvas) {
// basically there are 3 drawing modes
// 1 - simple drawing when nothing is cached.
// 2 - when we are caching current
// 3 - when node is cached and we need to draw it into layer
const layer = this.getLayer();
const canvas = can || layer!.getCanvas(),
context = canvas.getContext() as SceneContext,
cachedCanvas = this._getCanvasCache(),
drawFunc = this.getSceneFunc(),
hasShadow = this.hasShadow();
let stage;
const skipBuffer = false;
const cachingSelf = top === this;
if (!this.isVisible() && !cachingSelf) {
return this;
}
// if node is cached we just need to draw from cache
if (cachedCanvas) {
context.save();
const m = this.getAbsoluteTransform(top).getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
this._drawCachedSceneCanvas(context);
context.restore();
return this;
}
if (!drawFunc) {
return this;
}
context.save();
// if buffer canvas is needed
if (this._useBufferCanvas() && !skipBuffer) {
stage = this.getStage();
const bc = bufferCanvas || stage.bufferCanvas;
const bufferContext = bc.getContext();
// When caching, the buffer canvas may have a translation applied.
// We need to reset the transform before clearing to ensure the entire canvas is cleared.
if (bufferCanvas) {
bufferContext.save();
bufferContext.setTransform(1, 0, 0, 1, 0, 0);
bufferContext.clearRect(0, 0, bc.width, bc.height);
bufferContext.restore();
} else {
bufferContext.clear();
}
bufferContext.save();
bufferContext._applyLineJoin(this);
bufferContext._applyMiterLimit(this);
// layer might be undefined if we are using cache before adding to layer
const o = this.getAbsoluteTransform(top).getMatrix();
bufferContext.transform(o[0], o[1], o[2], o[3], o[4], o[5]);
// Apply CSS filters to buffer context if not cached
// we skip filters in non cache use cases for now
// if (!cachingSelf && filters?.length > 0) {
// bufferContext._applyCSSFilters(this);
// }
drawFunc.call(this, bufferContext, this);
bufferContext.restore();
const ratio = bc.pixelRatio;
if (hasShadow) {
context._applyShadow(this);
}
// if we are caching self, we don't need to apply opacity and global composite operation
// because it will be applied in the cache
if (!cachingSelf) {
context._applyOpacity(this);
context._applyGlobalCompositeOperation(this);
}
context.drawImage(
bc._canvas,
bc.x || 0,
bc.y || 0,
bc.width / ratio,
bc.height / ratio
);
} else {
context._applyLineJoin(this);
context._applyMiterLimit(this);
if (!cachingSelf) {
const o = this.getAbsoluteTransform(top).getMatrix();
context.transform(o[0], o[1], o[2], o[3], o[4], o[5]);
context._applyOpacity(this);
context._applyGlobalCompositeOperation(this);
// Apply CSS filters to main context if not cached
// if (filters?.length) {
// context._applyCSSFilters(this);
// }
}
if (hasShadow) {
context._applyShadow(this);
}
drawFunc.call(this, context, this);
}
context.restore();
return this;
}
drawHit(can?: HitCanvas, top?: Node, skipDragCheck = false) {
if (!this.shouldDrawHit(top, skipDragCheck)) {
return this;
}
const layer = this.getLayer(),
canvas = can || layer!.hitCanvas,
context = canvas && canvas.getContext(),
drawFunc = this.hitFunc() || this.sceneFunc(),
cachedCanvas = this._getCanvasCache(),
cachedHitCanvas = cachedCanvas && cachedCanvas.hit;
if (!this.colorKey) {
Util.warn(
'Looks like your canvas has a destroyed shape in it. Do not reuse shape after you destroyed it. If you want to reuse shape you should call remove() instead of destroy()'
);
}
if (cachedHitCanvas) {
context.save();
const m = this.getAbsoluteTransform(top).getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
this._drawCachedHitCanvas(context);
context.restore();
return this;
}
if (!drawFunc) {
return this;
}
context.save();
context._applyLineJoin(this);
context._applyMiterLimit(this);
const selfCache = this === top;
if (!selfCache) {
const o = this.getAbsoluteTransform(top).getMatrix();
context.transform(o[0], o[1], o[2], o[3], o[4], o[5]);
}
drawFunc.call(this, context, this);
context.restore();
return this;
}
/**
* draw hit graph using the cached scene canvas
* @method
* @name Konva.Shape#drawHitFromCache
* @param {Integer} alphaThreshold alpha channel threshold that determines whether or not
* a pixel should be drawn onto the hit graph. Must be a value between 0 and 255.
* The default is 0
* @returns {Konva.Shape}
* @example
* shape.cache();
* shape.drawHitFromCache();
*/
drawHitFromCache(alphaThreshold = 0) {
const cachedCanvas = this._getCanvasCache(),
sceneCanvas = this._getCachedSceneCanvas(),
hitCanvas = cachedCanvas.hit,
hitContext = hitCanvas.getContext(),
hitWidth = hitCanvas.getWidth(),
hitHeight = hitCanvas.getHeight();
hitContext.clear();
hitContext.drawImage(sceneCanvas._canvas, 0, 0, hitWidth, hitHeight);
try {
const hitImageData = hitContext.getImageData(0, 0, hitWidth, hitHeight);
const hitData = hitImageData.data;
const len = hitData.length;
const rgbColorKey = Util._hexToRgb(this.colorKey);
// replace non transparent pixels with color key
for (let i = 0; i < len; i += 4) {
const alpha = hitData[i + 3];
if (alpha > alphaThreshold) {
hitData[i] = rgbColorKey.r;
hitData[i + 1] = rgbColorKey.g;
hitData[i + 2] = rgbColorKey.b;
hitData[i + 3] = 255;
} else {
hitData[i + 3] = 0;
}
}
hitContext.putImageData(hitImageData, 0, 0);
} catch (e: any) {
Util.error(
'Unable to draw hit graph from cached scene canvas. ' + e.message
);
}
return this;
}
hasPointerCapture(pointerId: number): boolean {
return PointerEvents.hasPointerCapture(pointerId, this);
}
setPointerCapture(pointerId: number) {
PointerEvents.setPointerCapture(pointerId, this);
}
releaseCapture(pointerId: number) {
PointerEvents.releaseCapture(pointerId, this);
}
draggable: GetSet;
embossBlend: GetSet;
dash: GetSet;
dashEnabled: GetSet;
dashOffset: GetSet;
fill: GetSet;
fillEnabled: GetSet;
fillLinearGradientColorStops: GetSet, this>;
fillLinearGradientStartPoint: GetSet;
fillLinearGradientStartPointX: GetSet;
fillLinearGradientStartPointY: GetSet;
fillLinearGradientEndPoint: GetSet;
fillLinearGradientEndPointX: GetSet;
fillLinearGradientEndPointY: GetSet;
fillLinearRadialStartPoint: GetSet;
fillLinearRadialStartPointX: GetSet;
fillLinearRadialStartPointY: GetSet;
fillLinearRadialEndPoint: GetSet;
fillLinearRadialEndPointX: GetSet;
fillLinearRadialEndPointY: GetSet;
fillPatternImage: GetSet;
fillRadialGradientStartRadius: GetSet;
fillRadialGradientEndRadius: GetSet;
fillRadialGradientColorStops: GetSet, this>;
fillRadialGradientStartPoint: GetSet;
fillRadialGradientStartPointX: GetSet;
fillRadialGradientStartPointY: GetSet;
fillRadialGradientEndPoint: GetSet;
fillRadialGradientEndPointX: GetSet;
fillRadialGradientEndPointY: GetSet;
fillPatternOffset: GetSet;
fillPatternOffsetX: GetSet;
fillPatternOffsetY: GetSet;
fillPatternRepeat: GetSet;
fillPatternRotation: GetSet;
fillPatternScale: GetSet;
fillPatternScaleX: GetSet;
fillPatternScaleY: GetSet;
fillPatternX: GetSet;
fillPatternY: GetSet;
fillPriority: GetSet;
hitFunc: GetSet, this>;
lineCap: GetSet;
lineJoin: GetSet;
miterLimit: GetSet;
perfectDrawEnabled: GetSet;
sceneFunc: GetSet, this>;
shadowColor: GetSet;
shadowEnabled: GetSet;
shadowForStrokeEnabled: GetSet;
shadowOffset: GetSet;
shadowOffsetX: GetSet;
shadowOffsetY: GetSet;
shadowOpacity: GetSet;
shadowBlur: GetSet;
stroke: GetSet;
strokeEnabled: GetSet;
fillAfterStrokeEnabled: GetSet;
strokeScaleEnabled: GetSet;
strokeHitEnabled: GetSet;
strokeWidth: GetSet;
hitStrokeWidth: GetSet;
strokeLinearGradientStartPoint: GetSet;
strokeLinearGradientEndPoint: GetSet;
strokeLinearGradientColorStops: GetSet, this>;
strokeLinearGradientStartPointX: GetSet;
strokeLinearGradientStartPointY: GetSet;
strokeLinearGradientEndPointX: GetSet;
strokeLinearGradientEndPointY: GetSet;
fillRule: GetSet;
}
Shape.prototype._fillFunc = _fillFunc;
Shape.prototype._strokeFunc = _strokeFunc;
Shape.prototype._fillFuncHit = _fillFuncHit;
Shape.prototype._strokeFuncHit = _strokeFuncHit;
Shape.prototype._centroid = false;
Shape.prototype.nodeType = 'Shape';
_registerNode(Shape);
Shape.prototype.eventListeners = {};
Shape.prototype.on(
'shadowColorChange.konva shadowBlurChange.konva shadowOffsetChange.konva shadowOpacityChange.konva shadowEnabledChange.konva',
_clearHasShadowCache
);
Shape.prototype.on(
'shadowColorChange.konva shadowOpacityChange.konva shadowEnabledChange.konva',
_clearGetShadowRGBACache
);
Shape.prototype.on(
'fillPriorityChange.konva fillPatternImageChange.konva fillPatternRepeatChange.konva fillPatternScaleXChange.konva fillPatternScaleYChange.konva fillPatternOffsetXChange.konva fillPatternOffsetYChange.konva fillPatternXChange.konva fillPatternYChange.konva fillPatternRotationChange.konva',
_clearFillPatternCache
);
Shape.prototype.on(
'fillPriorityChange.konva fillLinearGradientColorStopsChange.konva fillLinearGradientStartPointXChange.konva fillLinearGradientStartPointYChange.konva fillLinearGradientEndPointXChange.konva fillLinearGradientEndPointYChange.konva',
_clearLinearGradientCache
);
Shape.prototype.on(
'fillPriorityChange.konva fillRadialGradientColorStopsChange.konva fillRadialGradientStartPointXChange.konva fillRadialGradientStartPointYChange.konva fillRadialGradientEndPointXChange.konva fillRadialGradientEndPointYChange.konva fillRadialGradientStartRadiusChange.konva fillRadialGradientEndRadiusChange.konva',
_clearRadialGradientCache
);
// add getters and setters
Factory.addGetterSetter(
Shape,
'stroke',
undefined,
getStringOrGradientValidator()
);
/**
* get/set stroke color
* @name Konva.Shape#stroke
* @method
* @param {String} color
* @returns {String}
* @example
* // get stroke color
* var stroke = shape.stroke();
*
* // set stroke color with color string
* shape.stroke('green');
*
* // set stroke color with hex
* shape.stroke('#00ff00');
*
* // set stroke color with rgb
* shape.stroke('rgb(0,255,0)');
*
* // set stroke color with rgba and make it 50% opaque
* shape.stroke('rgba(0,255,0,0.5');
*/
Factory.addGetterSetter(Shape, 'strokeWidth', 2, getNumberValidator());
/**
* get/set stroke width
* @name Konva.Shape#strokeWidth
* @method
* @param {Number} strokeWidth
* @returns {Number}
* @example
* // get stroke width
* var strokeWidth = shape.strokeWidth();
*
* // set stroke width
* shape.strokeWidth(10);
*/
Factory.addGetterSetter(Shape, 'fillAfterStrokeEnabled', false);
/**
* get/set fillAfterStrokeEnabled property. By default Konva is drawing filling first, then stroke on top of the fill.
* In rare situations you may want a different behavior. When you have a stroke first then fill on top of it.
* Especially useful for Text objects.
* Default is false.
* @name Konva.Shape#fillAfterStrokeEnabled
* @method
* @param {Boolean} fillAfterStrokeEnabled
* @returns {Boolean}
* @example
* // get stroke width
* var fillAfterStrokeEnabled = shape.fillAfterStrokeEnabled();
*
* // set stroke width
* shape.fillAfterStrokeEnabled(true);
*/
Factory.addGetterSetter(
Shape,
'hitStrokeWidth',
'auto',
getNumberOrAutoValidator()
);
/**
* get/set stroke width for hit detection. Default value is "auto", it means it will be equals to strokeWidth
* @name Konva.Shape#hitStrokeWidth
* @method
* @param {Number} hitStrokeWidth
* @returns {Number}
* @example
* // get stroke width
* var hitStrokeWidth = shape.hitStrokeWidth();
*
* // set hit stroke width
* shape.hitStrokeWidth(20);
* // set hit stroke width always equals to scene stroke width
* shape.hitStrokeWidth('auto');
*/
Factory.addGetterSetter(Shape, 'strokeHitEnabled', true, getBooleanValidator());
/**
* **deprecated, use hitStrokeWidth instead!** get/set strokeHitEnabled property. Useful for performance optimization.
* You may set `shape.strokeHitEnabled(false)`. In this case stroke will be no draw on hit canvas, so hit area
* of shape will be decreased (by lineWidth / 2). Remember that non closed line with `strokeHitEnabled = false`
* will be not drawn on hit canvas, that is mean line will no trigger pointer events (like mouseover)
* Default value is true.
* @name Konva.Shape#strokeHitEnabled
* @method
* @param {Boolean} strokeHitEnabled
* @returns {Boolean}
* @example
* // get strokeHitEnabled
* var strokeHitEnabled = shape.strokeHitEnabled();
*
* // set strokeHitEnabled
* shape.strokeHitEnabled();
*/
Factory.addGetterSetter(
Shape,
'perfectDrawEnabled',
true,
getBooleanValidator()
);
/**
* get/set perfectDrawEnabled. If a shape has fill, stroke and opacity you may set `perfectDrawEnabled` to false to improve performance.
* See http://konvajs.org/docs/performance/Disable_Perfect_Draw.html for more information.
* Default value is true
* @name Konva.Shape#perfectDrawEnabled
* @method
* @param {Boolean} perfectDrawEnabled
* @returns {Boolean}
* @example
* // get perfectDrawEnabled
* var perfectDrawEnabled = shape.perfectDrawEnabled();
*
* // set perfectDrawEnabled
* shape.perfectDrawEnabled();
*/
Factory.addGetterSetter(
Shape,
'shadowForStrokeEnabled',
true,
getBooleanValidator()
);
/**
* get/set shadowForStrokeEnabled. Useful for performance optimization.
* You may set `shape.shadowForStrokeEnabled(false)`. In this case stroke will no effect shadow.
* Remember if you set `shadowForStrokeEnabled = false` for non closed line - that line will have no shadow!.
* Default value is true
* @name Konva.Shape#shadowForStrokeEnabled
* @method
* @param {Boolean} shadowForStrokeEnabled
* @returns {Boolean}
* @example
* // get shadowForStrokeEnabled
* var shadowForStrokeEnabled = shape.shadowForStrokeEnabled();
*
* // set shadowForStrokeEnabled
* shape.shadowForStrokeEnabled();
*/
Factory.addGetterSetter(Shape, 'lineJoin');
/**
* get/set line join. Can be miter, round, or bevel. The
* default is miter
* @name Konva.Shape#lineJoin
* @method
* @param {String} lineJoin
* @returns {String}
* @example
* // get line join
* var lineJoin = shape.lineJoin();
*
* // set line join
* shape.lineJoin('round');
*/
Factory.addGetterSetter(Shape, 'lineCap');
/**
* get/set line cap. Can be butt, round, or square
* @name Konva.Shape#lineCap
* @method
* @param {String} lineCap
* @returns {String}
* @example
* // get line cap
* var lineCap = shape.lineCap();
*
* // set line cap
* shape.lineCap('round');
*/
Factory.addGetterSetter(Shape, 'miterLimit');
/**
* get/set miterLimit.
* @name Konva.Shape#miterLimit
* @method
* @param {Number} miterLimit
* @returns {Number}
* @example
* // get miter limit
* var miterLimit = shape.miterLimit();
*
* // set miter limit
* shape.miterLimit(10);
*/
Factory.addGetterSetter(Shape, 'sceneFunc');
/**
* get/set scene draw function. That function is used to draw the shape on a canvas.
* Also that function will be used to draw hit area of the shape, in case if hitFunc is not defined.
* @name Konva.Shape#sceneFunc
* @method
* @param {Function} drawFunc drawing function
* @returns {Function}
* @example
* // get scene draw function
* var sceneFunc = shape.sceneFunc();
*
* // set scene draw function
* shape.sceneFunc(function(context, shape) {
* context.beginPath();
* context.rect(0, 0, shape.width(), shape.height());
* context.closePath();
* // important Konva method that fill and stroke shape from its properties
* // like stroke and fill
* context.fillStrokeShape(shape);
* });
*/
Factory.addGetterSetter(Shape, 'hitFunc');
/**
* get/set hit draw function. That function is used to draw custom hit area of a shape.
* @name Konva.Shape#hitFunc
* @method
* @param {Function} drawFunc drawing function
* @returns {Function}
* @example
* // get hit draw function
* var hitFunc = shape.hitFunc();
*
* // set hit draw function
* shape.hitFunc(function(context) {
* context.beginPath();
* context.rect(0, 0, shape.width(), shape.height());
* context.closePath();
* // important Konva method that fill and stroke shape from its properties
* context.fillStrokeShape(shape);
* });
*/
Factory.addGetterSetter(Shape, 'dash');
/**
* get/set dash array for stroke.
* @name Konva.Shape#dash
* @method
* @param {Array} dash
* @returns {Array}
* @example
* // apply dashed stroke that is 10px long and 5 pixels apart
* line.dash([10, 5]);
* // apply dashed stroke that is made up of alternating dashed
* // lines that are 10px long and 20px apart, and dots that have
* // a radius of 5px and are 20px apart
* line.dash([10, 20, 0.001, 20]);
*/
Factory.addGetterSetter(Shape, 'dashOffset', 0, getNumberValidator());
/**
* get/set dash offset for stroke.
* @name Konva.Shape#dash
* @method
* @param {Number} dash offset
* @returns {Number}
* @example
* // apply dashed stroke that is 10px long and 5 pixels apart with an offset of 5px
* line.dash([10, 5]);
* line.dashOffset(5);
*/
Factory.addGetterSetter(Shape, 'shadowColor', undefined, getStringValidator());
/**
* get/set shadow color
* @name Konva.Shape#shadowColor
* @method
* @param {String} color
* @returns {String}
* @example
* // get shadow color
* var shadow = shape.shadowColor();
*
* // set shadow color with color string
* shape.shadowColor('green');
*
* // set shadow color with hex
* shape.shadowColor('#00ff00');
*
* // set shadow color with rgb
* shape.shadowColor('rgb(0,255,0)');
*
* // set shadow color with rgba and make it 50% opaque
* shape.shadowColor('rgba(0,255,0,0.5');
*/
Factory.addGetterSetter(Shape, 'shadowBlur', 0, getNumberValidator());
/**
* get/set shadow blur
* @name Konva.Shape#shadowBlur
* @method
* @param {Number} blur
* @returns {Number}
* @example
* // get shadow blur
* var shadowBlur = shape.shadowBlur();
*
* // set shadow blur
* shape.shadowBlur(10);
*/
Factory.addGetterSetter(Shape, 'shadowOpacity', 1, getNumberValidator());
/**
* get/set shadow opacity. must be a value between 0 and 1
* @name Konva.Shape#shadowOpacity
* @method
* @param {Number} opacity
* @returns {Number}
* @example
* // get shadow opacity
* var shadowOpacity = shape.shadowOpacity();
*
* // set shadow opacity
* shape.shadowOpacity(0.5);
*/
Factory.addComponentsGetterSetter(Shape, 'shadowOffset', ['x', 'y']);
/**
* get/set shadow offset
* @name Konva.Shape#shadowOffset
* @method
* @param {Object} offset
* @param {Number} offset.x
* @param {Number} offset.y
* @returns {Object}
* @example
* // get shadow offset
* var shadowOffset = shape.shadowOffset();
*
* // set shadow offset
* shape.shadowOffset({
* x: 20,
* y: 10
* });
*/
Factory.addGetterSetter(Shape, 'shadowOffsetX', 0, getNumberValidator());
/**
* get/set shadow offset x
* @name Konva.Shape#shadowOffsetX
* @method
* @param {Number} x
* @returns {Number}
* @example
* // get shadow offset x
* var shadowOffsetX = shape.shadowOffsetX();
*
* // set shadow offset x
* shape.shadowOffsetX(5);
*/
Factory.addGetterSetter(Shape, 'shadowOffsetY', 0, getNumberValidator());
/**
* get/set shadow offset y
* @name Konva.Shape#shadowOffsetY
* @method
* @param {Number} y
* @returns {Number}
* @example
* // get shadow offset y
* var shadowOffsetY = shape.shadowOffsetY();
*
* // set shadow offset y
* shape.shadowOffsetY(5);
*/
Factory.addGetterSetter(Shape, 'fillPatternImage');
/**
* get/set fill pattern image
* @name Konva.Shape#fillPatternImage
* @method
* @param {Image} image object
* @returns {Image}
* @example
* // get fill pattern image
* var fillPatternImage = shape.fillPatternImage();
*
* // set fill pattern image
* var imageObj = new Image();
* imageObj.onload = function() {
* shape.fillPatternImage(imageObj);
* };
* imageObj.src = 'path/to/image/jpg';
*/
Factory.addGetterSetter(
Shape,
'fill',
undefined,
getStringOrGradientValidator()
);
/**
* get/set fill color
* @name Konva.Shape#fill
* @method
* @param {String} color
* @returns {String}
* @example
* // get fill color
* var fill = shape.fill();
*
* // set fill color with color string
* shape.fill('green');
*
* // set fill color with hex
* shape.fill('#00ff00');
*
* // set fill color with rgb
* shape.fill('rgb(0,255,0)');
*
* // set fill color with rgba and make it 50% opaque
* shape.fill('rgba(0,255,0,0.5');
*
* // shape without fill
* shape.fill(null);
*/
Factory.addGetterSetter(Shape, 'fillPatternX', 0, getNumberValidator());
/**
* get/set fill pattern x
* @name Konva.Shape#fillPatternX
* @method
* @param {Number} x
* @returns {Number}
* @example
* // get fill pattern x
* var fillPatternX = shape.fillPatternX();
* // set fill pattern x
* shape.fillPatternX(20);
*/
Factory.addGetterSetter(Shape, 'fillPatternY', 0, getNumberValidator());
/**
* get/set fill pattern y
* @name Konva.Shape#fillPatternY
* @method
* @param {Number} y
* @returns {Number}
* @example
* // get fill pattern y
* var fillPatternY = shape.fillPatternY();
* // set fill pattern y
* shape.fillPatternY(20);
*/
Factory.addGetterSetter(Shape, 'fillLinearGradientColorStops');
/**
* get/set fill linear gradient color stops
* @name Konva.Shape#fillLinearGradientColorStops
* @method
* @param {Array} colorStops
* @returns {Array} colorStops
* @example
* // get fill linear gradient color stops
* var colorStops = shape.fillLinearGradientColorStops();
*
* // create a linear gradient that starts with red, changes to blue
* // halfway through, and then changes to green
* shape.fillLinearGradientColorStops(0, 'red', 0.5, 'blue', 1, 'green');
*/
Factory.addGetterSetter(Shape, 'strokeLinearGradientColorStops');
/**
* get/set stroke linear gradient color stops
* @name Konva.Shape#strokeLinearGradientColorStops
* @method
* @param {Array} colorStops
* @returns {Array} colorStops
* @example
* // get stroke linear gradient color stops
* var colorStops = shape.strokeLinearGradientColorStops();
*
* // create a linear gradient that starts with red, changes to blue
* // halfway through, and then changes to green
* shape.strokeLinearGradientColorStops([0, 'red', 0.5, 'blue', 1, 'green']);
*/
Factory.addGetterSetter(Shape, 'fillRadialGradientStartRadius', 0);
/**
* get/set fill radial gradient start radius
* @name Konva.Shape#fillRadialGradientStartRadius
* @method
* @param {Number} radius
* @returns {Number}
* @example
* // get radial gradient start radius
* var startRadius = shape.fillRadialGradientStartRadius();
*
* // set radial gradient start radius
* shape.fillRadialGradientStartRadius(0);
*/
Factory.addGetterSetter(Shape, 'fillRadialGradientEndRadius', 0);
/**
* get/set fill radial gradient end radius
* @name Konva.Shape#fillRadialGradientEndRadius
* @method
* @param {Number} radius
* @returns {Number}
* @example
* // get radial gradient end radius
* var endRadius = shape.fillRadialGradientEndRadius();
*
* // set radial gradient end radius
* shape.fillRadialGradientEndRadius(100);
*/
Factory.addGetterSetter(Shape, 'fillRadialGradientColorStops');
/**
* get/set fill radial gradient color stops
* @name Konva.Shape#fillRadialGradientColorStops
* @method
* @param {Number} colorStops
* @returns {Array}
* @example
* // get fill radial gradient color stops
* var colorStops = shape.fillRadialGradientColorStops();
*
* // create a radial gradient that starts with red, changes to blue
* // halfway through, and then changes to green
* shape.fillRadialGradientColorStops(0, 'red', 0.5, 'blue', 1, 'green');
*/
Factory.addGetterSetter(Shape, 'fillPatternRepeat', 'repeat');
/**
* get/set fill pattern repeat. Can be 'repeat', 'repeat-x', 'repeat-y', or 'no-repeat'. The default is 'repeat'
* @name Konva.Shape#fillPatternRepeat
* @method
* @param {String} repeat
* @returns {String}
* @example
* // get fill pattern repeat
* var repeat = shape.fillPatternRepeat();
*
* // repeat pattern in x direction only
* shape.fillPatternRepeat('repeat-x');
*
* // do not repeat the pattern
* shape.fillPatternRepeat('no-repeat');
*/
Factory.addGetterSetter(Shape, 'fillEnabled', true);
/**
* get/set fill enabled flag
* @name Konva.Shape#fillEnabled
* @method
* @param {Boolean} enabled
* @returns {Boolean}
* @example
* // get fill enabled flag
* var fillEnabled = shape.fillEnabled();
*
* // disable fill
* shape.fillEnabled(false);
*
* // enable fill
* shape.fillEnabled(true);
*/
Factory.addGetterSetter(Shape, 'strokeEnabled', true);
/**
* get/set stroke enabled flag
* @name Konva.Shape#strokeEnabled
* @method
* @param {Boolean} enabled
* @returns {Boolean}
* @example
* // get stroke enabled flag
* var strokeEnabled = shape.strokeEnabled();
*
* // disable stroke
* shape.strokeEnabled(false);
*
* // enable stroke
* shape.strokeEnabled(true);
*/
Factory.addGetterSetter(Shape, 'shadowEnabled', true);
/**
* get/set shadow enabled flag
* @name Konva.Shape#shadowEnabled
* @method
* @param {Boolean} enabled
* @returns {Boolean}
* @example
* // get shadow enabled flag
* var shadowEnabled = shape.shadowEnabled();
*
* // disable shadow
* shape.shadowEnabled(false);
*
* // enable shadow
* shape.shadowEnabled(true);
*/
Factory.addGetterSetter(Shape, 'dashEnabled', true);
/**
* get/set dash enabled flag
* @name Konva.Shape#dashEnabled
* @method
* @param {Boolean} enabled
* @returns {Boolean}
* @example
* // get dash enabled flag
* var dashEnabled = shape.dashEnabled();
*
* // disable dash
* shape.dashEnabled(false);
*
* // enable dash
* shape.dashEnabled(true);
*/
Factory.addGetterSetter(Shape, 'strokeScaleEnabled', true);
/**
* get/set strokeScale enabled flag
* @name Konva.Shape#strokeScaleEnabled
* @method
* @param {Boolean} enabled
* @returns {Boolean}
* @example
* // get stroke scale enabled flag
* var strokeScaleEnabled = shape.strokeScaleEnabled();
*
* // disable stroke scale
* shape.strokeScaleEnabled(false);
*
* // enable stroke scale
* shape.strokeScaleEnabled(true);
*/
Factory.addGetterSetter(Shape, 'fillPriority', 'color');
/**
* get/set fill priority. can be color, pattern, linear-gradient, or radial-gradient. The default is color.
* This is handy if you want to toggle between different fill types.
* @name Konva.Shape#fillPriority
* @method
* @param {String} priority
* @returns {String}
* @example
* // get fill priority
* var fillPriority = shape.fillPriority();
*
* // set fill priority
* shape.fillPriority('linear-gradient');
*/
Factory.addComponentsGetterSetter(Shape, 'fillPatternOffset', ['x', 'y']);
/**
* get/set fill pattern offset
* @name Konva.Shape#fillPatternOffset
* @method
* @param {Object} offset
* @param {Number} offset.x
* @param {Number} offset.y
* @returns {Object}
* @example
* // get fill pattern offset
* var patternOffset = shape.fillPatternOffset();
*
* // set fill pattern offset
* shape.fillPatternOffset({
* x: 20,
* y: 10
* });
*/
Factory.addGetterSetter(Shape, 'fillPatternOffsetX', 0, getNumberValidator());
/**
* get/set fill pattern offset x
* @name Konva.Shape#fillPatternOffsetX
* @method
* @param {Number} x
* @returns {Number}
* @example
* // get fill pattern offset x
* var patternOffsetX = shape.fillPatternOffsetX();
*
* // set fill pattern offset x
* shape.fillPatternOffsetX(20);
*/
Factory.addGetterSetter(Shape, 'fillPatternOffsetY', 0, getNumberValidator());
/**
* get/set fill pattern offset y
* @name Konva.Shape#fillPatternOffsetY
* @method
* @param {Number} y
* @returns {Number}
* @example
* // get fill pattern offset y
* var patternOffsetY = shape.fillPatternOffsetY();
*
* // set fill pattern offset y
* shape.fillPatternOffsetY(10);
*/
Factory.addComponentsGetterSetter(Shape, 'fillPatternScale', ['x', 'y']);
/**
* get/set fill pattern scale
* @name Konva.Shape#fillPatternScale
* @method
* @param {Object} scale
* @param {Number} scale.x
* @param {Number} scale.y
* @returns {Object}
* @example
* // get fill pattern scale
* var patternScale = shape.fillPatternScale();
*
* // set fill pattern scale
* shape.fillPatternScale({
* x: 2,
* y: 2
* });
*/
Factory.addGetterSetter(Shape, 'fillPatternScaleX', 1, getNumberValidator());
/**
* get/set fill pattern scale x
* @name Konva.Shape#fillPatternScaleX
* @method
* @param {Number} x
* @returns {Number}
* @example
* // get fill pattern scale x
* var patternScaleX = shape.fillPatternScaleX();
*
* // set fill pattern scale x
* shape.fillPatternScaleX(2);
*/
Factory.addGetterSetter(Shape, 'fillPatternScaleY', 1, getNumberValidator());
/**
* get/set fill pattern scale y
* @name Konva.Shape#fillPatternScaleY
* @method
* @param {Number} y
* @returns {Number}
* @example
* // get fill pattern scale y
* var patternScaleY = shape.fillPatternScaleY();
*
* // set fill pattern scale y
* shape.fillPatternScaleY(2);
*/
Factory.addComponentsGetterSetter(Shape, 'fillLinearGradientStartPoint', [
'x',
'y',
]);
/**
* get/set fill linear gradient start point
* @name Konva.Shape#fillLinearGradientStartPoint
* @method
* @param {Object} startPoint
* @param {Number} startPoint.x
* @param {Number} startPoint.y
* @returns {Object}
* @example
* // get fill linear gradient start point
* var startPoint = shape.fillLinearGradientStartPoint();
*
* // set fill linear gradient start point
* shape.fillLinearGradientStartPoint({
* x: 20,
* y: 10
* });
*/
Factory.addComponentsGetterSetter(Shape, 'strokeLinearGradientStartPoint', [
'x',
'y',
]);
/**
* get/set stroke linear gradient start point
* @name Konva.Shape#strokeLinearGradientStartPoint
* @method
* @param {Object} startPoint
* @param {Number} startPoint.x
* @param {Number} startPoint.y
* @returns {Object}
* @example
* // get stroke linear gradient start point
* var startPoint = shape.strokeLinearGradientStartPoint();
*
* // set stroke linear gradient start point
* shape.strokeLinearGradientStartPoint({
* x: 20,
* y: 10
* });
*/
Factory.addGetterSetter(Shape, 'fillLinearGradientStartPointX', 0);
/**
* get/set fill linear gradient start point x
* @name Konva.Shape#fillLinearGradientStartPointX
* @method
* @param {Number} x
* @returns {Number}
* @example
* // get fill linear gradient start point x
* var startPointX = shape.fillLinearGradientStartPointX();
*
* // set fill linear gradient start point x
* shape.fillLinearGradientStartPointX(20);
*/
Factory.addGetterSetter(Shape, 'strokeLinearGradientStartPointX', 0);
/**
* get/set stroke linear gradient start point x
* @name Konva.Shape#linearLinearGradientStartPointX
* @method
* @param {Number} x
* @returns {Number}
* @example
* // get stroke linear gradient start point x
* var startPointX = shape.strokeLinearGradientStartPointX();
*
* // set stroke linear gradient start point x
* shape.strokeLinearGradientStartPointX(20);
*/
Factory.addGetterSetter(Shape, 'fillLinearGradientStartPointY', 0);
/**
* get/set fill linear gradient start point y
* @name Konva.Shape#fillLinearGradientStartPointY
* @method
* @param {Number} y
* @returns {Number}
* @example
* // get fill linear gradient start point y
* var startPointY = shape.fillLinearGradientStartPointY();
*
* // set fill linear gradient start point y
* shape.fillLinearGradientStartPointY(20);
*/
Factory.addGetterSetter(Shape, 'strokeLinearGradientStartPointY', 0);
/**
* get/set stroke linear gradient start point y
* @name Konva.Shape#strokeLinearGradientStartPointY
* @method
* @param {Number} y
* @returns {Number}
* @example
* // get stroke linear gradient start point y
* var startPointY = shape.strokeLinearGradientStartPointY();
*
* // set stroke linear gradient start point y
* shape.strokeLinearGradientStartPointY(20);
*/
Factory.addComponentsGetterSetter(Shape, 'fillLinearGradientEndPoint', [
'x',
'y',
]);
/**
* get/set fill linear gradient end point
* @name Konva.Shape#fillLinearGradientEndPoint
* @method
* @param {Object} endPoint
* @param {Number} endPoint.x
* @param {Number} endPoint.y
* @returns {Object}
* @example
* // get fill linear gradient end point
* var endPoint = shape.fillLinearGradientEndPoint();
*
* // set fill linear gradient end point
* shape.fillLinearGradientEndPoint({
* x: 20,
* y: 10
* });
*/
Factory.addComponentsGetterSetter(Shape, 'strokeLinearGradientEndPoint', [
'x',
'y',
]);
/**
* get/set stroke linear gradient end point
* @name Konva.Shape#strokeLinearGradientEndPoint
* @method
* @param {Object} endPoint
* @param {Number} endPoint.x
* @param {Number} endPoint.y
* @returns {Object}
* @example
* // get stroke linear gradient end point
* var endPoint = shape.strokeLinearGradientEndPoint();
*
* // set stroke linear gradient end point
* shape.strokeLinearGradientEndPoint({
* x: 20,
* y: 10
* });
*/
Factory.addGetterSetter(Shape, 'fillLinearGradientEndPointX', 0);
/**
* get/set fill linear gradient end point x
* @name Konva.Shape#fillLinearGradientEndPointX
* @method
* @param {Number} x
* @returns {Number}
* @example
* // get fill linear gradient end point x
* var endPointX = shape.fillLinearGradientEndPointX();
*
* // set fill linear gradient end point x
* shape.fillLinearGradientEndPointX(20);
*/
Factory.addGetterSetter(Shape, 'strokeLinearGradientEndPointX', 0);
/**
* get/set fill linear gradient end point x
* @name Konva.Shape#strokeLinearGradientEndPointX
* @method
* @param {Number} x
* @returns {Number}
* @example
* // get stroke linear gradient end point x
* var endPointX = shape.strokeLinearGradientEndPointX();
*
* // set stroke linear gradient end point x
* shape.strokeLinearGradientEndPointX(20);
*/
Factory.addGetterSetter(Shape, 'fillLinearGradientEndPointY', 0);
/**
* get/set fill linear gradient end point y
* @name Konva.Shape#fillLinearGradientEndPointY
* @method
* @param {Number} y
* @returns {Number}
* @example
* // get fill linear gradient end point y
* var endPointY = shape.fillLinearGradientEndPointY();
*
* // set fill linear gradient end point y
* shape.fillLinearGradientEndPointY(20);
*/
Factory.addGetterSetter(Shape, 'strokeLinearGradientEndPointY', 0);
/**
* get/set stroke linear gradient end point y
* @name Konva.Shape#strokeLinearGradientEndPointY
* @method
* @param {Number} y
* @returns {Number}
* @example
* // get stroke linear gradient end point y
* var endPointY = shape.strokeLinearGradientEndPointY();
*
* // set stroke linear gradient end point y
* shape.strokeLinearGradientEndPointY(20);
*/
Factory.addComponentsGetterSetter(Shape, 'fillRadialGradientStartPoint', [
'x',
'y',
]);
/**
* get/set fill radial gradient start point
* @name Konva.Shape#fillRadialGradientStartPoint
* @method
* @param {Object} startPoint
* @param {Number} startPoint.x
* @param {Number} startPoint.y
* @returns {Object}
* @example
* // get fill radial gradient start point
* var startPoint = shape.fillRadialGradientStartPoint();
*
* // set fill radial gradient start point
* shape.fillRadialGradientStartPoint({
* x: 20,
* y: 10
* });
*/
Factory.addGetterSetter(Shape, 'fillRadialGradientStartPointX', 0);
/**
* get/set fill radial gradient start point x
* @name Konva.Shape#fillRadialGradientStartPointX
* @method
* @param {Number} x
* @returns {Number}
* @example
* // get fill radial gradient start point x
* var startPointX = shape.fillRadialGradientStartPointX();
*
* // set fill radial gradient start point x
* shape.fillRadialGradientStartPointX(20);
*/
Factory.addGetterSetter(Shape, 'fillRadialGradientStartPointY', 0);
/**
* get/set fill radial gradient start point y
* @name Konva.Shape#fillRadialGradientStartPointY
* @method
* @param {Number} y
* @returns {Number}
* @example
* // get fill radial gradient start point y
* var startPointY = shape.fillRadialGradientStartPointY();
*
* // set fill radial gradient start point y
* shape.fillRadialGradientStartPointY(20);
*/
Factory.addComponentsGetterSetter(Shape, 'fillRadialGradientEndPoint', [
'x',
'y',
]);
/**
* get/set fill radial gradient end point
* @name Konva.Shape#fillRadialGradientEndPoint
* @method
* @param {Object} endPoint
* @param {Number} endPoint.x
* @param {Number} endPoint.y
* @returns {Object}
* @example
* // get fill radial gradient end point
* var endPoint = shape.fillRadialGradientEndPoint();
*
* // set fill radial gradient end point
* shape.fillRadialGradientEndPoint({
* x: 20,
* y: 10
* });
*/
Factory.addGetterSetter(Shape, 'fillRadialGradientEndPointX', 0);
/**
* get/set fill radial gradient end point x
* @name Konva.Shape#fillRadialGradientEndPointX
* @method
* @param {Number} x
* @returns {Number}
* @example
* // get fill radial gradient end point x
* var endPointX = shape.fillRadialGradientEndPointX();
*
* // set fill radial gradient end point x
* shape.fillRadialGradientEndPointX(20);
*/
Factory.addGetterSetter(Shape, 'fillRadialGradientEndPointY', 0);
/**
* get/set fill radial gradient end point y
* @name Konva.Shape#fillRadialGradientEndPointY
* @method
* @param {Number} y
* @returns {Number}
* @example
* // get fill radial gradient end point y
* var endPointY = shape.fillRadialGradientEndPointY();
*
* // set fill radial gradient end point y
* shape.fillRadialGradientEndPointY(20);
*/
Factory.addGetterSetter(Shape, 'fillPatternRotation', 0);
/**
* get/set fill pattern rotation in degrees
* @name Konva.Shape#fillPatternRotation
* @method
* @param {Number} rotation
* @returns {Konva.Shape}
* @example
* // get fill pattern rotation
* var patternRotation = shape.fillPatternRotation();
*
* // set fill pattern rotation
* shape.fillPatternRotation(20);
*/
Factory.addGetterSetter(Shape, 'fillRule', undefined, getStringValidator());
/**
* get/set fill rule
* @name Konva.Shape#fillRule
* @method
* @param {CanvasFillRule} rotation
* @returns {Konva.Shape}
* @example
* // get fill rule
* var fillRule = shape.fillRule();
*
* // set fill rule
* shape.fillRule('evenodd');
*/
Factory.backCompat(Shape, {
dashArray: 'dash',
getDashArray: 'getDash',
setDashArray: 'getDash',
drawFunc: 'sceneFunc',
getDrawFunc: 'getSceneFunc',
setDrawFunc: 'setSceneFunc',
drawHitFunc: 'hitFunc',
getDrawHitFunc: 'getHitFunc',
setDrawHitFunc: 'setHitFunc',
});
================================================
FILE: src/Stage.ts
================================================
import { Util } from './Util.ts';
import { Factory } from './Factory.ts';
import type { ContainerConfig } from './Container.ts';
import { Container } from './Container.ts';
import { Konva } from './Global.ts';
import { SceneCanvas, HitCanvas } from './Canvas.ts';
import type { GetSet, Vector2d } from './types.ts';
import type { Shape } from './Shape.ts';
import type { Layer } from './Layer.ts';
import { DD } from './DragAndDrop.ts';
import { _registerNode } from './Global.ts';
import * as PointerEvents from './PointerEvents.ts';
export interface StageConfig extends ContainerConfig {
container?: HTMLDivElement | string;
}
// CONSTANTS
const STAGE = 'Stage',
STRING = 'string',
PX = 'px',
MOUSEOUT = 'mouseout',
MOUSELEAVE = 'mouseleave',
MOUSEOVER = 'mouseover',
MOUSEENTER = 'mouseenter',
MOUSEMOVE = 'mousemove',
MOUSEDOWN = 'mousedown',
MOUSEUP = 'mouseup',
POINTERMOVE = 'pointermove',
POINTERDOWN = 'pointerdown',
POINTERUP = 'pointerup',
POINTERCANCEL = 'pointercancel',
LOSTPOINTERCAPTURE = 'lostpointercapture',
POINTEROUT = 'pointerout',
POINTERLEAVE = 'pointerleave',
POINTEROVER = 'pointerover',
POINTERENTER = 'pointerenter',
CONTEXTMENU = 'contextmenu',
TOUCHSTART = 'touchstart',
TOUCHEND = 'touchend',
TOUCHMOVE = 'touchmove',
TOUCHCANCEL = 'touchcancel',
WHEEL = 'wheel',
MAX_LAYERS_NUMBER = 5,
EVENTS = [
[MOUSEENTER, '_pointerenter'],
[MOUSEDOWN, '_pointerdown'],
[MOUSEMOVE, '_pointermove'],
[MOUSEUP, '_pointerup'],
[MOUSELEAVE, '_pointerleave'],
[TOUCHSTART, '_pointerdown'],
[TOUCHMOVE, '_pointermove'],
[TOUCHEND, '_pointerup'],
[TOUCHCANCEL, '_pointercancel'],
[MOUSEOVER, '_pointerover'],
[WHEEL, '_wheel'],
[CONTEXTMENU, '_contextmenu'],
[POINTERDOWN, '_pointerdown'],
[POINTERMOVE, '_pointermove'],
[POINTERUP, '_pointerup'],
[POINTERCANCEL, '_pointercancel'],
[POINTERLEAVE, '_pointerleave'],
[LOSTPOINTERCAPTURE, '_lostpointercapture'],
];
const EVENTS_MAP = {
mouse: {
[POINTEROUT]: MOUSEOUT,
[POINTERLEAVE]: MOUSELEAVE,
[POINTEROVER]: MOUSEOVER,
[POINTERENTER]: MOUSEENTER,
[POINTERMOVE]: MOUSEMOVE,
[POINTERDOWN]: MOUSEDOWN,
[POINTERUP]: MOUSEUP,
[POINTERCANCEL]: 'mousecancel',
pointerclick: 'click',
pointerdblclick: 'dblclick',
},
touch: {
[POINTEROUT]: 'touchout',
[POINTERLEAVE]: 'touchleave',
[POINTEROVER]: 'touchover',
[POINTERENTER]: 'touchenter',
[POINTERMOVE]: TOUCHMOVE,
[POINTERDOWN]: TOUCHSTART,
[POINTERUP]: TOUCHEND,
[POINTERCANCEL]: TOUCHCANCEL,
pointerclick: 'tap',
pointerdblclick: 'dbltap',
},
pointer: {
[POINTEROUT]: POINTEROUT,
[POINTERLEAVE]: POINTERLEAVE,
[POINTEROVER]: POINTEROVER,
[POINTERENTER]: POINTERENTER,
[POINTERMOVE]: POINTERMOVE,
[POINTERDOWN]: POINTERDOWN,
[POINTERUP]: POINTERUP,
[POINTERCANCEL]: POINTERCANCEL,
pointerclick: 'pointerclick',
pointerdblclick: 'pointerdblclick',
},
};
const getEventType = (type) => {
if (type.indexOf('pointer') >= 0) {
return 'pointer';
}
if (type.indexOf('touch') >= 0) {
return 'touch';
}
return 'mouse';
};
const getEventsMap = (eventType: string) => {
const type = getEventType(eventType);
if (type === 'pointer') {
return Konva.pointerEventsEnabled && EVENTS_MAP.pointer;
}
if (type === 'touch') {
return EVENTS_MAP.touch;
}
if (type === 'mouse') {
return EVENTS_MAP.mouse;
}
};
function checkNoClip(attrs: any = {}) {
if (attrs.clipFunc || attrs.clipWidth || attrs.clipHeight) {
Util.warn(
'Stage does not support clipping. Please use clip for Layers or Groups.'
);
}
return attrs;
}
const NO_POINTERS_MESSAGE = `Pointer position is missing and not registered by the stage. Looks like it is outside of the stage container. You can set it manually from event: stage.setPointersPositions(event);`;
export const stages: Stage[] = [];
/**
* Stage constructor. A stage is used to contain multiple layers
* @constructor
* @memberof Konva
* @augments Konva.Container
* @param {Object} config
* @param {String|Element} config.container Container selector or DOM element
* @@nodeParams
* @example
* var stage = new Konva.Stage({
* width: 500,
* height: 800,
* container: 'containerId' // or "#containerId" or ".containerClass"
* });
*/
export class Stage extends Container {
content: HTMLDivElement;
pointerPos: Vector2d | null;
_pointerPositions: (Vector2d & { id?: number })[] = [];
_changedPointerPositions: (Vector2d & { id: number })[] = [];
bufferCanvas: SceneCanvas;
bufferHitCanvas: HitCanvas;
_mouseTargetShape: Shape;
_touchTargetShape: Shape;
_pointerTargetShape: Shape;
_mouseClickStartShape: Shape;
_touchClickStartShape: Shape;
_pointerClickStartShape: Shape;
_mouseClickEndShape: Shape;
_touchClickEndShape: Shape;
_pointerClickEndShape: Shape;
_mouseDblTimeout: any;
_touchDblTimeout: any;
_pointerDblTimeout: any;
constructor(config: StageConfig) {
super(checkNoClip(config));
this._buildDOM();
this._bindContentEvents();
stages.push(this);
this.on('widthChange.konva heightChange.konva', this._resizeDOM);
this.on('visibleChange.konva', this._checkVisibility);
this.on(
'clipWidthChange.konva clipHeightChange.konva clipFuncChange.konva',
() => {
checkNoClip(this.attrs);
}
);
this._checkVisibility();
}
_validateAdd(child) {
const isLayer = child.getType() === 'Layer';
const isFastLayer = child.getType() === 'FastLayer';
const valid = isLayer || isFastLayer;
if (!valid) {
Util.throw('You may only add layers to the stage.');
}
}
_checkVisibility() {
if (!this.content) {
return;
}
const style = this.visible() ? '' : 'none';
this.content.style.display = style;
}
/**
* set container dom element which contains the stage wrapper div element
* @method
* @name Konva.Stage#setContainer
* @param {DomElement} container can pass in a dom element or id string
*/
setContainer(container) {
if (typeof container === STRING) {
let id;
if (container.charAt(0) === '.') {
const className = container.slice(1);
container = document.getElementsByClassName(className)[0];
} else {
if (container.charAt(0) !== '#') {
id = container;
} else {
id = container.slice(1);
}
container = document.getElementById(id);
}
if (!container) {
throw 'Can not find container in document with id ' + id;
}
}
this._setAttr('container', container);
if (this.content) {
if (this.content.parentElement) {
this.content.parentElement.removeChild(this.content);
}
container.appendChild(this.content);
}
return this;
}
shouldDrawHit() {
return true;
}
/**
* clear all layers
* @method
* @name Konva.Stage#clear
*/
clear() {
const layers = this.children,
len = layers.length;
for (let n = 0; n < len; n++) {
layers[n].clear();
}
return this;
}
clone(obj?) {
if (!obj) {
obj = {};
}
obj.container =
typeof document !== 'undefined' && document.createElement('div');
return Container.prototype.clone.call(this, obj) as this;
}
destroy() {
super.destroy();
const content = this.content;
if (content && Util._isInDocument(content)) {
this.container().removeChild(content);
}
const index = stages.indexOf(this);
if (index > -1) {
stages.splice(index, 1);
}
Util.releaseCanvas(this.bufferCanvas._canvas, this.bufferHitCanvas._canvas);
return this;
}
/**
* returns ABSOLUTE pointer position which can be a touch position or mouse position
* pointer position doesn't include any transforms (such as scale) of the stage
* it is just a plain position of pointer relative to top-left corner of the canvas
* @method
* @name Konva.Stage#getPointerPosition
* @returns {Vector2d|null}
*/
getPointerPosition(): Vector2d | null {
const pos = this._pointerPositions[0] || this._changedPointerPositions[0];
if (!pos) {
Util.warn(NO_POINTERS_MESSAGE);
return null;
}
return {
x: pos.x,
y: pos.y,
};
}
_getPointerById(id?: number) {
return this._pointerPositions.find((p) => p.id === id);
}
getPointersPositions() {
return this._pointerPositions;
}
getStage() {
return this;
}
getContent() {
return this.content;
}
_toKonvaCanvas(config) {
config = { ...config };
config.x = config.x || 0;
config.y = config.y || 0;
config.width = config.width || this.width();
config.height = config.height || this.height();
const canvas = new SceneCanvas({
width: config.width,
height: config.height,
pixelRatio: config.pixelRatio || 1,
});
const _context = canvas.getContext()._context;
const layers = this.children;
if (config.x || config.y) {
_context.translate(-1 * config.x, -1 * config.y);
}
layers.forEach(function (layer) {
if (!layer.isVisible()) {
return;
}
const layerCanvas = layer._toKonvaCanvas(config);
_context.drawImage(
layerCanvas._canvas,
config.x,
config.y,
layerCanvas.getWidth() / layerCanvas.getPixelRatio(),
layerCanvas.getHeight() / layerCanvas.getPixelRatio()
);
});
return canvas;
}
/**
* get visible intersection shape. This is the preferred
* method for determining if a point intersects a shape or not
* nodes with listening set to false will not be detected
* @method
* @name Konva.Stage#getIntersection
* @param {Object} pos
* @param {Number} pos.x
* @param {Number} pos.y
* @returns {Konva.Node}
* @example
* var shape = stage.getIntersection({x: 50, y: 50});
*/
getIntersection(pos: Vector2d) {
if (!pos) {
return null;
}
const layers = this.children,
len = layers.length,
end = len - 1;
for (let n = end; n >= 0; n--) {
const shape = layers[n].getIntersection(pos);
if (shape) {
return shape;
}
}
return null;
}
_resizeDOM() {
const width = this.width();
const height = this.height();
if (this.content) {
// set content dimensions
this.content.style.width = width + PX;
this.content.style.height = height + PX;
}
this.bufferCanvas.setSize(width, height);
this.bufferHitCanvas.setSize(width, height);
// set layer dimensions
this.children.forEach((layer) => {
layer.setSize({ width, height });
layer.draw();
});
}
add(layer: Layer, ...rest) {
if (arguments.length > 1) {
for (let i = 0; i < arguments.length; i++) {
this.add(arguments[i]);
}
return this;
}
super.add(layer);
const length = this.children.length;
if (length > MAX_LAYERS_NUMBER) {
Util.warn(
'The stage has ' +
length +
' layers. Recommended maximum number of layers is 3-5. Adding more layers into the stage may drop the performance. Rethink your tree structure, you can use Konva.Group.'
);
}
layer.setSize({ width: this.width(), height: this.height() });
// draw layer and append canvas to container
layer.draw();
if (Konva.isBrowser) {
this.content.appendChild(layer.canvas._canvas);
}
// chainable
return this;
}
getParent() {
return null;
}
getLayer() {
return null;
}
hasPointerCapture(pointerId: number): boolean {
return PointerEvents.hasPointerCapture(pointerId, this);
}
setPointerCapture(pointerId: number) {
PointerEvents.setPointerCapture(pointerId, this);
}
releaseCapture(pointerId: number) {
PointerEvents.releaseCapture(pointerId, this);
}
/**
* returns an array of layers
* @method
* @name Konva.Stage#getLayers
*/
getLayers() {
return this.children;
}
_bindContentEvents() {
if (!Konva.isBrowser) {
return;
}
EVENTS.forEach(([event, methodName]) => {
this.content.addEventListener(
event,
(evt) => {
this[methodName](evt);
},
{ passive: false }
);
});
}
_pointerenter(evt: PointerEvent) {
this.setPointersPositions(evt);
const events = getEventsMap(evt.type);
if (events) {
this._fire(events.pointerenter, {
evt: evt,
target: this,
currentTarget: this,
});
}
}
_pointerover(evt) {
this.setPointersPositions(evt);
const events = getEventsMap(evt.type);
if (events) {
this._fire(events.pointerover, {
evt: evt,
target: this,
currentTarget: this,
});
}
}
_getTargetShape(evenType) {
let shape: Shape | null = this[evenType + 'targetShape'];
if (shape && !shape.getStage()) {
shape = null;
}
return shape;
}
_pointerleave(evt) {
const events = getEventsMap(evt.type);
const eventType = getEventType(evt.type);
if (!events) {
return;
}
this.setPointersPositions(evt);
const targetShape = this._getTargetShape(eventType);
const eventsEnabled =
!(Konva.isDragging() || Konva.isTransforming()) || Konva.hitOnDragEnabled;
if (targetShape && eventsEnabled) {
targetShape._fireAndBubble(events.pointerout, { evt: evt });
targetShape._fireAndBubble(events.pointerleave, { evt: evt });
this._fire(events.pointerleave, {
evt: evt,
target: this,
currentTarget: this,
});
this[eventType + 'targetShape'] = null;
} else if (eventsEnabled) {
this._fire(events.pointerleave, {
evt: evt,
target: this,
currentTarget: this,
});
this._fire(events.pointerout, {
evt: evt,
target: this,
currentTarget: this,
});
}
this.pointerPos = null;
this._pointerPositions = [];
}
_pointerdown(evt: TouchEvent | MouseEvent | PointerEvent) {
const events = getEventsMap(evt.type);
const eventType = getEventType(evt.type);
if (!events) {
return;
}
this.setPointersPositions(evt);
let triggeredOnShape = false;
this._changedPointerPositions.forEach((pos) => {
const shape = this.getIntersection(pos);
DD.justDragged = false;
// probably we are staring a click
Konva['_' + eventType + 'ListenClick'] = true;
// no shape detected? do nothing
if (!shape || !shape.isListening()) {
this[eventType + 'ClickStartShape'] = undefined;
return;
}
if (Konva.capturePointerEventsEnabled) {
shape.setPointerCapture(pos.id);
}
// save where we started the click
this[eventType + 'ClickStartShape'] = shape;
shape._fireAndBubble(events.pointerdown, {
evt: evt,
pointerId: pos.id,
});
triggeredOnShape = true;
// TODO: test in iframe
// only call preventDefault if the shape is listening for events
const isTouch = evt.type.indexOf('touch') >= 0;
if (shape.preventDefault() && evt.cancelable && isTouch) {
evt.preventDefault();
}
});
// trigger down on stage if not already
if (!triggeredOnShape) {
this._fire(events.pointerdown, {
evt: evt,
target: this,
currentTarget: this,
pointerId: this._pointerPositions[0].id,
});
}
}
_pointermove(evt: TouchEvent | MouseEvent | PointerEvent) {
const events = getEventsMap(evt.type);
const eventType = getEventType(evt.type);
if (!events) {
return;
}
// prevent default only for touch-based interactions to avoid blocking
// native mouse wheel scrolling during drag on desktop
const isTouchPointer =
(evt as any).type.indexOf('touch') >= 0 ||
(evt as any).pointerType === 'touch';
if (
Konva.isDragging() &&
DD.node!.preventDefault() &&
evt.cancelable &&
isTouchPointer
) {
evt.preventDefault();
}
this.setPointersPositions(evt);
const eventsEnabled =
!(Konva.isDragging() || Konva.isTransforming()) || Konva.hitOnDragEnabled;
if (!eventsEnabled) {
return;
}
const processedShapesIds = {};
let triggeredOnShape = false;
const targetShape = this._getTargetShape(eventType);
this._changedPointerPositions.forEach((pos) => {
const shape = (PointerEvents.getCapturedShape(pos.id) ||
this.getIntersection(pos)) as Shape;
const pointerId = pos.id;
const event = { evt: evt, pointerId };
const differentTarget = targetShape !== shape;
if (differentTarget && targetShape) {
targetShape._fireAndBubble(events.pointerout, { ...event }, shape);
targetShape._fireAndBubble(events.pointerleave, { ...event }, shape);
}
if (shape) {
if (processedShapesIds[shape._id]) {
return;
}
processedShapesIds[shape._id] = true;
}
if (shape && shape.isListening()) {
triggeredOnShape = true;
if (differentTarget) {
shape._fireAndBubble(events.pointerover, { ...event }, targetShape);
shape._fireAndBubble(events.pointerenter, { ...event }, targetShape);
this[eventType + 'targetShape'] = shape;
}
shape._fireAndBubble(events.pointermove, { ...event });
} else {
if (targetShape) {
this._fire(events.pointerover, {
evt: evt,
target: this,
currentTarget: this,
pointerId,
});
this[eventType + 'targetShape'] = null;
}
}
});
if (!triggeredOnShape) {
this._fire(events.pointermove, {
evt: evt,
target: this,
currentTarget: this,
pointerId: this._changedPointerPositions[0].id,
});
}
}
_pointerup(evt) {
const events = getEventsMap(evt.type);
const eventType = getEventType(evt.type);
if (!events) {
return;
}
this.setPointersPositions(evt);
const clickStartShape = this[eventType + 'ClickStartShape'];
const clickEndShape = this[eventType + 'ClickEndShape'];
const processedShapesIds = {};
let skipPointerUpTrigger = false;
this._changedPointerPositions.forEach((pos) => {
const shape = (PointerEvents.getCapturedShape(pos.id) ||
this.getIntersection(pos)) as Shape;
if (shape) {
shape.releaseCapture(pos.id);
if (processedShapesIds[shape._id]) {
return;
}
processedShapesIds[shape._id] = true;
}
const pointerId = pos.id;
const event = { evt: evt, pointerId };
let fireDblClick = false;
if (Konva['_' + eventType + 'InDblClickWindow']) {
fireDblClick = true;
clearTimeout(this[eventType + 'DblTimeout']);
} else if (!DD.justDragged) {
// don't set inDblClickWindow after dragging
Konva['_' + eventType + 'InDblClickWindow'] = true;
clearTimeout(this[eventType + 'DblTimeout']);
}
this[eventType + 'DblTimeout'] = setTimeout(function () {
Konva['_' + eventType + 'InDblClickWindow'] = false;
}, Konva.dblClickWindow);
if (shape && shape.isListening()) {
skipPointerUpTrigger = true;
this[eventType + 'ClickEndShape'] = shape;
shape._fireAndBubble(events.pointerup, { ...event });
// detect if click or double click occurred
if (
Konva['_' + eventType + 'ListenClick'] &&
clickStartShape &&
clickStartShape === shape
) {
shape._fireAndBubble(events.pointerclick, { ...event });
if (fireDblClick && clickEndShape && clickEndShape === shape) {
shape._fireAndBubble(events.pointerdblclick, { ...event });
}
}
} else {
this[eventType + 'ClickEndShape'] = null;
if (!skipPointerUpTrigger) {
this._fire(events.pointerup, {
evt: evt,
target: this,
currentTarget: this,
pointerId: this._changedPointerPositions[0].id,
});
skipPointerUpTrigger = true;
}
if (Konva['_' + eventType + 'ListenClick']) {
this._fire(events.pointerclick, {
evt: evt,
target: this,
currentTarget: this,
pointerId,
});
}
if (fireDblClick) {
this._fire(events.pointerdblclick, {
evt: evt,
target: this,
currentTarget: this,
pointerId,
});
}
}
});
if (!skipPointerUpTrigger) {
this._fire(events.pointerup, {
evt: evt,
target: this,
currentTarget: this,
pointerId: this._changedPointerPositions[0].id,
});
}
Konva['_' + eventType + 'ListenClick'] = false;
// always call preventDefault for desktop events because some browsers
// try to drag and drop the canvas element
// TODO: are we sure we need to prevent default at all?
// do not call this function on mobile because it prevent "click" event on all parent containers
// but apps may listen to it.
if (evt.cancelable && eventType !== 'touch' && eventType !== 'pointer') {
evt.preventDefault();
}
}
_contextmenu(evt) {
this.setPointersPositions(evt);
const shape = this.getIntersection(this.getPointerPosition()!);
if (shape && shape.isListening()) {
shape._fireAndBubble(CONTEXTMENU, { evt: evt });
} else {
this._fire(CONTEXTMENU, {
evt: evt,
target: this,
currentTarget: this,
});
}
}
_wheel(evt) {
this.setPointersPositions(evt);
const shape = this.getIntersection(this.getPointerPosition()!);
if (shape && shape.isListening()) {
shape._fireAndBubble(WHEEL, { evt: evt });
} else {
this._fire(WHEEL, {
evt: evt,
target: this,
currentTarget: this,
});
}
}
_pointercancel(evt: PointerEvent) {
this.setPointersPositions(evt);
const shape =
PointerEvents.getCapturedShape(evt.pointerId) ||
this.getIntersection(this.getPointerPosition()!);
if (shape) {
shape._fireAndBubble(POINTERUP, PointerEvents.createEvent(evt));
}
PointerEvents.releaseCapture(evt.pointerId);
}
_lostpointercapture(evt: PointerEvent) {
PointerEvents.releaseCapture(evt.pointerId);
}
/**
* manually register pointers positions (mouse/touch) in the stage.
* So you can use stage.getPointerPosition(). Usually you don't need to use that method
* because all internal events are automatically registered. It may be useful if event
* is triggered outside of the stage, but you still want to use Konva methods to get pointers position.
* @method
* @name Konva.Stage#setPointersPositions
* @param {Object} event Event object
* @example
*
* window.addEventListener('mousemove', (e) => {
* stage.setPointersPositions(e);
* });
*/
setPointersPositions(evt) {
const contentPosition = this._getContentPosition();
let x: number | null = null,
y: number | null = null;
evt = evt ? evt : window.event;
// touch events
if (evt.touches !== undefined) {
// touchlist has not support for map method
// so we have to iterate
this._pointerPositions = [];
this._changedPointerPositions = [];
Array.prototype.forEach.call(evt.touches, (touch: any) => {
this._pointerPositions.push({
id: touch.identifier,
x: (touch.clientX - contentPosition.left) / contentPosition.scaleX,
y: (touch.clientY - contentPosition.top) / contentPosition.scaleY,
});
});
Array.prototype.forEach.call(
evt.changedTouches || evt.touches,
(touch: any) => {
this._changedPointerPositions.push({
id: touch.identifier,
x: (touch.clientX - contentPosition.left) / contentPosition.scaleX,
y: (touch.clientY - contentPosition.top) / contentPosition.scaleY,
});
}
);
} else {
// mouse events
x = (evt.clientX - contentPosition.left) / contentPosition.scaleX;
y = (evt.clientY - contentPosition.top) / contentPosition.scaleY;
this.pointerPos = {
x: x,
y: y,
};
this._pointerPositions = [{ x, y, id: Util._getFirstPointerId(evt) }];
this._changedPointerPositions = [
{ x, y, id: Util._getFirstPointerId(evt) },
];
}
}
_setPointerPosition(evt) {
Util.warn(
'Method _setPointerPosition is deprecated. Use "stage.setPointersPositions(event)" instead.'
);
this.setPointersPositions(evt);
}
_getContentPosition() {
if (!this.content || !this.content.getBoundingClientRect) {
return {
top: 0,
left: 0,
scaleX: 1,
scaleY: 1,
};
}
const rect = this.content.getBoundingClientRect();
return {
top: rect.top,
left: rect.left,
// sometimes clientWidth can be equals to 0
// i saw it in react-konva test, looks like it is because of hidden testing element
scaleX: rect.width / this.content.clientWidth || 1,
scaleY: rect.height / this.content.clientHeight || 1,
};
}
_buildDOM() {
this.bufferCanvas = new SceneCanvas({
width: this.width(),
height: this.height(),
});
this.bufferHitCanvas = new HitCanvas({
pixelRatio: 1,
width: this.width(),
height: this.height(),
});
if (!Konva.isBrowser) {
return;
}
const container = this.container();
if (!container) {
throw 'Stage has no container. A container is required.';
}
// clear content inside container
container.innerHTML = '';
// content
this.content = document.createElement('div');
this.content.style.position = 'relative';
this.content.style.userSelect = 'none';
this.content.className = 'konvajs-content';
this.content.setAttribute('role', 'presentation');
container.appendChild(this.content);
this._resizeDOM();
}
// currently cache function is now working for stage, because stage has no its own canvas element
cache() {
Util.warn(
'Cache function is not allowed for stage. You may use cache only for layers, groups and shapes.'
);
return this;
}
clearCache() {
return this;
}
/**
* batch draw
* @method
* @name Konva.Stage#batchDraw
* @return {Konva.Stage} this
*/
batchDraw() {
this.getChildren().forEach(function (layer) {
layer.batchDraw();
});
return this;
}
container: GetSet;
}
Stage.prototype.nodeType = STAGE;
_registerNode(Stage);
/**
* get/set container DOM element
* @method
* @name Konva.Stage#container
* @returns {DomElement} container
* @example
* // get container
* var container = stage.container();
* // set container
* var container = document.createElement('div');
* body.appendChild(container);
* stage.container(container);
*/
Factory.addGetterSetter(Stage, 'container');
// chrome is clearing canvas in inactive browser window, causing layer content to be erased
// so let's redraw layers as soon as window becomes active
// TODO: any other way to solve this issue?
// TODO: should we remove it if chrome fixes the issue?
if (Konva.isBrowser) {
document.addEventListener('visibilitychange', () => {
stages.forEach((stage) => {
stage.batchDraw();
});
});
}
================================================
FILE: src/Tween.ts
================================================
import { Util } from './Util.ts';
import { Animation } from './Animation.ts';
import type { NodeConfig } from './Node.ts';
import { Node } from './Node.ts';
import { Konva } from './Global.ts';
import type { Line } from './shapes/Line.ts';
const blacklist = {
node: 1,
duration: 1,
easing: 1,
onFinish: 1,
yoyo: 1,
},
PAUSED = 1,
PLAYING = 2,
REVERSING = 3,
colorAttrs = ['fill', 'stroke', 'shadowColor'];
let idCounter = 0;
class TweenEngine {
prop: string;
propFunc: Function;
begin: number;
_pos: number;
duration: number;
prevPos: number;
yoyo: boolean;
_time: number;
_position: number;
_startTime: number;
_finish: number;
func: Function;
_change: number;
state: number;
onPlay: Function;
onReverse: Function;
onPause: Function;
onReset: Function;
onFinish: Function;
onUpdate: Function;
constructor(prop, propFunc, func, begin, finish, duration, yoyo) {
this.prop = prop;
this.propFunc = propFunc;
this.begin = begin;
this._pos = begin;
this.duration = duration;
this._change = 0;
this.prevPos = 0;
this.yoyo = yoyo;
this._time = 0;
this._position = 0;
this._startTime = 0;
this._finish = 0;
this.func = func;
this._change = finish - this.begin;
this.pause();
}
fire(str) {
const handler = this[str];
if (handler) {
handler();
}
}
setTime(t) {
if (t > this.duration) {
if (this.yoyo) {
this._time = this.duration;
this.reverse();
} else {
this.finish();
}
} else if (t < 0) {
if (this.yoyo) {
this._time = 0;
this.play();
} else {
this.reset();
}
} else {
this._time = t;
this.update();
}
}
getTime() {
return this._time;
}
setPosition(p) {
this.prevPos = this._pos;
this.propFunc(p);
this._pos = p;
}
getPosition(t) {
if (t === undefined) {
t = this._time;
}
return this.func(t, this.begin, this._change, this.duration);
}
play() {
this.state = PLAYING;
this._startTime = this.getTimer() - this._time;
this.onEnterFrame();
this.fire('onPlay');
}
reverse() {
this.state = REVERSING;
this._time = this.duration - this._time;
this._startTime = this.getTimer() - this._time;
this.onEnterFrame();
this.fire('onReverse');
}
seek(t) {
this.pause();
this._time = t;
this.update();
this.fire('onSeek');
}
reset() {
this.pause();
this._time = 0;
this.update();
this.fire('onReset');
}
finish() {
this.pause();
this._time = this.duration;
this.update();
this.fire('onFinish');
}
update() {
this.setPosition(this.getPosition(this._time));
this.fire('onUpdate');
}
onEnterFrame() {
const t = this.getTimer() - this._startTime;
if (this.state === PLAYING) {
this.setTime(t);
} else if (this.state === REVERSING) {
this.setTime(this.duration - t);
}
}
pause() {
this.state = PAUSED;
this.fire('onPause');
}
getTimer() {
return new Date().getTime();
}
}
export interface TweenConfig extends NodeConfig {
easing?: (typeof Easings)[keyof typeof Easings];
yoyo?: boolean;
onReset?: Function;
onFinish?: Function;
onUpdate?: Function;
duration?: number;
node: Node;
}
/**
* Tween constructor. Tweens enable you to animate a node between the current state and a new state.
* You can play, pause, reverse, seek, reset, and finish tweens. By default, tweens are animated using
* a linear easing. For more tweening options, check out {@link Konva.Easings}
* @constructor
* @memberof Konva
* @example
* // instantiate new tween which fully rotates a node in 1 second
* var tween = new Konva.Tween({
* // list of tween specific properties
* node: node,
* duration: 1,
* easing: Konva.Easings.EaseInOut,
* onUpdate: () => console.log('node attrs updated')
* onFinish: () => console.log('finished'),
* // set new values for any attributes of a passed node
* rotation: 360,
* fill: 'red'
* });
*
* // play tween
* tween.play();
*
* // pause tween
* tween.pause();
*/
export class Tween {
static attrs = {};
static tweens = {};
node: Node;
anim: Animation;
tween: TweenEngine;
_id: number;
onFinish: Function | undefined;
onReset: Function | undefined;
onUpdate: Function | undefined;
constructor(config: TweenConfig) {
const that = this,
node = config.node as any,
nodeId = node._id,
easing = config.easing || Easings.Linear,
yoyo = !!config.yoyo;
let duration, key;
if (typeof config.duration === 'undefined') {
duration = 0.3;
} else if (config.duration === 0) {
// zero is bad value for duration
duration = 0.001;
} else {
duration = config.duration;
}
this.node = node;
this._id = idCounter++;
const layers =
node.getLayer() ||
(node instanceof Konva['Stage'] ? node.getLayers() : null);
if (!layers) {
Util.error(
'Tween constructor have `node` that is not in a layer. Please add node into layer first.'
);
}
this.anim = new Animation(function () {
that.tween.onEnterFrame();
}, layers);
this.tween = new TweenEngine(
key,
function (i) {
that._tweenFunc(i);
},
easing,
0,
1,
duration * 1000,
yoyo
);
this._addListeners();
// init attrs map
if (!Tween.attrs[nodeId]) {
Tween.attrs[nodeId] = {};
}
if (!Tween.attrs[nodeId][this._id]) {
Tween.attrs[nodeId][this._id] = {};
}
// init tweens map
if (!Tween.tweens[nodeId]) {
Tween.tweens[nodeId] = {};
}
for (key in config) {
if (blacklist[key] === undefined) {
this._addAttr(key, config[key]);
}
}
this.reset();
// callbacks
this.onFinish = config.onFinish;
this.onReset = config.onReset;
this.onUpdate = config.onUpdate;
}
_addAttr(key, end) {
const node = this.node,
nodeId = node._id;
let diff, len, trueEnd, trueStart, endRGBA;
// remove conflict from tween map if it exists
const tweenId = Tween.tweens[nodeId][key];
if (tweenId) {
delete Tween.attrs[nodeId][tweenId][key];
}
// add to tween map
let start = node.getAttr(key);
if (Util._isArray(end)) {
diff = [];
len = Math.max(end.length, start.length);
if (key === 'points' && end.length !== start.length) {
// before tweening points we need to make sure that start.length === end.length
// Util._prepareArrayForTween thinking that end.length > start.length
if (end.length > start.length) {
// so in this case we will increase number of starting points
trueStart = start;
start = Util._prepareArrayForTween(
start,
end,
(node as Line).closed()
);
} else {
// in this case we will increase number of eding points
trueEnd = end;
end = Util._prepareArrayForTween(end, start, (node as Line).closed());
}
}
if (key.indexOf('fill') === 0) {
for (let n = 0; n < len; n++) {
if (n % 2 === 0) {
diff.push(end[n] - start[n]);
} else {
const startRGBA = Util.colorToRGBA(start[n])!;
endRGBA = Util.colorToRGBA(end[n]);
start[n] = startRGBA;
diff.push({
r: endRGBA.r - startRGBA.r,
g: endRGBA.g - startRGBA.g,
b: endRGBA.b - startRGBA.b,
a: endRGBA.a - startRGBA.a,
});
}
}
} else {
for (let n = 0; n < len; n++) {
diff.push(end[n] - start[n]);
}
}
} else if (colorAttrs.indexOf(key) !== -1) {
start = Util.colorToRGBA(start);
endRGBA = Util.colorToRGBA(end);
diff = {
r: endRGBA.r - start.r,
g: endRGBA.g - start.g,
b: endRGBA.b - start.b,
a: endRGBA.a - start.a,
};
} else {
diff = end - start;
}
Tween.attrs[nodeId][this._id][key] = {
start: start,
diff: diff,
end: end,
trueEnd: trueEnd,
trueStart: trueStart,
};
Tween.tweens[nodeId][key] = this._id;
}
_tweenFunc(i) {
const node = this.node,
attrs = Tween.attrs[node._id][this._id];
let key, attr, start, diff, newVal, n, len, end;
for (key in attrs) {
attr = attrs[key];
start = attr.start;
diff = attr.diff;
end = attr.end;
if (Util._isArray(start)) {
newVal = [];
len = Math.max(start.length, end.length);
if (key.indexOf('fill') === 0) {
for (n = 0; n < len; n++) {
if (n % 2 === 0) {
newVal.push((start[n] || 0) + diff[n] * i);
} else {
newVal.push(
'rgba(' +
Math.round(start[n].r + diff[n].r * i) +
',' +
Math.round(start[n].g + diff[n].g * i) +
',' +
Math.round(start[n].b + diff[n].b * i) +
',' +
(start[n].a + diff[n].a * i) +
')'
);
}
}
} else {
for (n = 0; n < len; n++) {
newVal.push((start[n] || 0) + diff[n] * i);
}
}
} else if (colorAttrs.indexOf(key) !== -1) {
newVal =
'rgba(' +
Math.round(start.r + diff.r * i) +
',' +
Math.round(start.g + diff.g * i) +
',' +
Math.round(start.b + diff.b * i) +
',' +
(start.a + diff.a * i) +
')';
} else {
newVal = start + diff * i;
}
node.setAttr(key, newVal);
}
}
_addListeners() {
// start listeners
this.tween.onPlay = () => {
this.anim.start();
};
this.tween.onReverse = () => {
this.anim.start();
};
// stop listeners
this.tween.onPause = () => {
this.anim.stop();
};
this.tween.onFinish = () => {
const node = this.node as Node;
// after tweening points of line we need to set original end
const attrs = Tween.attrs[node._id][this._id];
if (attrs.points && attrs.points.trueEnd) {
node.setAttr('points' as any, attrs.points.trueEnd);
}
if (this.onFinish) {
this.onFinish.call(this);
}
};
this.tween.onReset = () => {
const node = this.node as any;
// after tweening points of line we need to set original start
const attrs = Tween.attrs[node._id][this._id];
if (attrs.points && attrs.points.trueStart) {
node.points(attrs.points.trueStart);
}
if (this.onReset) {
this.onReset();
}
};
this.tween.onUpdate = () => {
if (this.onUpdate) {
this.onUpdate.call(this);
}
};
}
/**
* play
* @method
* @name Konva.Tween#play
* @returns {Tween}
*/
play() {
this.tween.play();
return this;
}
/**
* reverse
* @method
* @name Konva.Tween#reverse
* @returns {Tween}
*/
reverse() {
this.tween.reverse();
return this;
}
/**
* reset
* @method
* @name Konva.Tween#reset
* @returns {Tween}
*/
reset() {
this.tween.reset();
return this;
}
/**
* seek
* @method
* @name Konva.Tween#seek(
* @param {Integer} t time in seconds between 0 and the duration
* @returns {Tween}
*/
seek(t) {
this.tween.seek(t * 1000);
return this;
}
/**
* pause
* @method
* @name Konva.Tween#pause
* @returns {Tween}
*/
pause() {
this.tween.pause();
return this;
}
/**
* finish
* @method
* @name Konva.Tween#finish
* @returns {Tween}
*/
finish() {
this.tween.finish();
return this;
}
/**
* destroy
* @method
* @name Konva.Tween#destroy
*/
destroy() {
const nodeId = this.node._id,
thisId = this._id,
attrs = Tween.tweens[nodeId];
this.pause();
// Clean up animation
if (this.anim) {
this.anim.stop();
}
// Clean up tween entries
for (const key in attrs) {
delete Tween.tweens[nodeId][key];
}
// Clean up attrs entry
delete Tween.attrs[nodeId][thisId];
// Clean up parent objects if empty
if (Tween.tweens[nodeId]) {
if (Object.keys(Tween.tweens[nodeId]).length === 0) {
delete Tween.tweens[nodeId];
}
if (Object.keys(Tween.attrs[nodeId]).length === 0) {
delete Tween.attrs[nodeId];
}
}
}
}
/**
* Tween node properties. Shorter usage of {@link Konva.Tween} object.
*
* @method Konva.Node#to
* @param {Object} [params] tween params
* @example
*
* circle.to({
* x : 50,
* duration : 0.5,
* onUpdate: () => console.log('props updated'),
* onFinish: () => console.log('finished'),
* });
*/
Node.prototype.to = function (params) {
const onFinish = params.onFinish;
params.node = this;
params.onFinish = function () {
this.destroy();
if (onFinish) {
onFinish();
}
};
const tween = new Tween(params as any);
tween.play();
};
/*
* These eases were ported from an Adobe Flash tweening library to JavaScript
* by Xaric
*/
/**
* @namespace Easings
* @memberof Konva
*/
export const Easings = {
/**
* back ease in
* @function
* @memberof Konva.Easings
*/
BackEaseIn(t, b, c, d) {
const s = 1.70158;
return c * (t /= d) * t * ((s + 1) * t - s) + b;
},
/**
* back ease out
* @function
* @memberof Konva.Easings
*/
BackEaseOut(t, b, c, d) {
const s = 1.70158;
return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
},
/**
* back ease in out
* @function
* @memberof Konva.Easings
*/
BackEaseInOut(t, b, c, d) {
let s = 1.70158;
if ((t /= d / 2) < 1) {
return (c / 2) * (t * t * (((s *= 1.525) + 1) * t - s)) + b;
}
return (c / 2) * ((t -= 2) * t * (((s *= 1.525) + 1) * t + s) + 2) + b;
},
/**
* elastic ease in
* @function
* @memberof Konva.Easings
*/
ElasticEaseIn(t, b, c, d, a, p) {
// added s = 0
let s = 0;
if (t === 0) {
return b;
}
if ((t /= d) === 1) {
return b + c;
}
if (!p) {
p = d * 0.3;
}
if (!a || a < Math.abs(c)) {
a = c;
s = p / 4;
} else {
s = (p / (2 * Math.PI)) * Math.asin(c / a);
}
return (
-(
a *
Math.pow(2, 10 * (t -= 1)) *
Math.sin(((t * d - s) * (2 * Math.PI)) / p)
) + b
);
},
/**
* elastic ease out
* @function
* @memberof Konva.Easings
*/
ElasticEaseOut(t, b, c, d, a, p) {
// added s = 0
let s = 0;
if (t === 0) {
return b;
}
if ((t /= d) === 1) {
return b + c;
}
if (!p) {
p = d * 0.3;
}
if (!a || a < Math.abs(c)) {
a = c;
s = p / 4;
} else {
s = (p / (2 * Math.PI)) * Math.asin(c / a);
}
return (
a * Math.pow(2, -10 * t) * Math.sin(((t * d - s) * (2 * Math.PI)) / p) +
c +
b
);
},
/**
* elastic ease in out
* @function
* @memberof Konva.Easings
*/
ElasticEaseInOut(t, b, c, d, a, p) {
// added s = 0
let s = 0;
if (t === 0) {
return b;
}
if ((t /= d / 2) === 2) {
return b + c;
}
if (!p) {
p = d * (0.3 * 1.5);
}
if (!a || a < Math.abs(c)) {
a = c;
s = p / 4;
} else {
s = (p / (2 * Math.PI)) * Math.asin(c / a);
}
if (t < 1) {
return (
-0.5 *
(a *
Math.pow(2, 10 * (t -= 1)) *
Math.sin(((t * d - s) * (2 * Math.PI)) / p)) +
b
);
}
return (
a *
Math.pow(2, -10 * (t -= 1)) *
Math.sin(((t * d - s) * (2 * Math.PI)) / p) *
0.5 +
c +
b
);
},
/**
* bounce ease out
* @function
* @memberof Konva.Easings
*/
BounceEaseOut(t, b, c, d) {
if ((t /= d) < 1 / 2.75) {
return c * (7.5625 * t * t) + b;
} else if (t < 2 / 2.75) {
return c * (7.5625 * (t -= 1.5 / 2.75) * t + 0.75) + b;
} else if (t < 2.5 / 2.75) {
return c * (7.5625 * (t -= 2.25 / 2.75) * t + 0.9375) + b;
} else {
return c * (7.5625 * (t -= 2.625 / 2.75) * t + 0.984375) + b;
}
},
/**
* bounce ease in
* @function
* @memberof Konva.Easings
*/
BounceEaseIn(t, b, c, d) {
return c - Easings.BounceEaseOut(d - t, 0, c, d) + b;
},
/**
* bounce ease in out
* @function
* @memberof Konva.Easings
*/
BounceEaseInOut(t, b, c, d) {
if (t < d / 2) {
return Easings.BounceEaseIn(t * 2, 0, c, d) * 0.5 + b;
} else {
return Easings.BounceEaseOut(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b;
}
},
/**
* ease in
* @function
* @memberof Konva.Easings
*/
EaseIn(t, b, c, d) {
return c * (t /= d) * t + b;
},
/**
* ease out
* @function
* @memberof Konva.Easings
*/
EaseOut(t, b, c, d) {
return -c * (t /= d) * (t - 2) + b;
},
/**
* ease in out
* @function
* @memberof Konva.Easings
*/
EaseInOut(t, b, c, d) {
if ((t /= d / 2) < 1) {
return (c / 2) * t * t + b;
}
return (-c / 2) * (--t * (t - 2) - 1) + b;
},
/**
* strong ease in
* @function
* @memberof Konva.Easings
*/
StrongEaseIn(t, b, c, d) {
return c * (t /= d) * t * t * t * t + b;
},
/**
* strong ease out
* @function
* @memberof Konva.Easings
*/
StrongEaseOut(t, b, c, d) {
return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
},
/**
* strong ease in out
* @function
* @memberof Konva.Easings
*/
StrongEaseInOut(t, b, c, d) {
if ((t /= d / 2) < 1) {
return (c / 2) * t * t * t * t * t + b;
}
return (c / 2) * ((t -= 2) * t * t * t * t + 2) + b;
},
/**
* linear
* @function
* @memberof Konva.Easings
*/
Linear(t, b, c, d) {
return (c * t) / d + b;
},
};
================================================
FILE: src/Util.ts
================================================
import { Konva } from './Global.ts';
import type { Context } from './Context.ts';
import type { IRect, RGB, Vector2d } from './types.ts';
const NODE_ERROR = `Konva.js unsupported environment.
Looks like you are trying to use Konva.js in Node.js environment. because "document" object is undefined.
To use Konva.js in Node.js environment, you need to use the "canvas-backend" or "skia-backend" module.
bash: npm install canvas
js: import "konva/canvas-backend";
or
bash: npm install skia-canvas
js: import "konva/skia-backend";
`;
const ensureBrowser = () => {
if (typeof document === 'undefined') {
throw new Error(NODE_ERROR);
}
};
/*
* Last updated November 2011
* By Simon Sarris
* www.simonsarris.com
* sarris@acm.org
*
* Free to use and distribute at will
* So long as you are nice to people, etc
*/
/*
* The usage of this class was inspired by some of the work done by a forked
* project, KineticJS-Ext by Wappworks, which is based on Simon's Transform
* class. Modified by Eric Rowell
*/
/**
* Transform constructor.
* In most of the cases you don't need to use it in your app. Because it is for internal usage in Konva core.
* But there is a documentation for that class in case you still want
* to make some manual calculations.
* @constructor
* @param {Array} [m] Optional six-element matrix
* @memberof Konva
*/
export class Transform {
m: Array;
dirty = false;
constructor(m = [1, 0, 0, 1, 0, 0]) {
this.m = (m && m.slice()) || [1, 0, 0, 1, 0, 0];
}
reset() {
this.m[0] = 1;
this.m[1] = 0;
this.m[2] = 0;
this.m[3] = 1;
this.m[4] = 0;
this.m[5] = 0;
}
/**
* Copy Konva.Transform object
* @method
* @name Konva.Transform#copy
* @returns {Konva.Transform}
* @example
* const tr = shape.getTransform().copy()
*/
copy() {
return new Transform(this.m);
}
copyInto(tr: Transform) {
tr.m[0] = this.m[0];
tr.m[1] = this.m[1];
tr.m[2] = this.m[2];
tr.m[3] = this.m[3];
tr.m[4] = this.m[4];
tr.m[5] = this.m[5];
}
/**
* Transform point
* @method
* @name Konva.Transform#point
* @param {Object} point 2D point(x, y)
* @returns {Object} 2D point(x, y)
*/
point(point: Vector2d) {
const m = this.m;
return {
x: m[0] * point.x + m[2] * point.y + m[4],
y: m[1] * point.x + m[3] * point.y + m[5],
};
}
/**
* Apply translation
* @method
* @name Konva.Transform#translate
* @param {Number} x
* @param {Number} y
* @returns {Konva.Transform}
*/
translate(x: number, y: number) {
this.m[4] += this.m[0] * x + this.m[2] * y;
this.m[5] += this.m[1] * x + this.m[3] * y;
return this;
}
/**
* Apply scale
* @method
* @name Konva.Transform#scale
* @param {Number} sx
* @param {Number} sy
* @returns {Konva.Transform}
*/
scale(sx: number, sy: number) {
this.m[0] *= sx;
this.m[1] *= sx;
this.m[2] *= sy;
this.m[3] *= sy;
return this;
}
/**
* Apply rotation
* @method
* @name Konva.Transform#rotate
* @param {Number} rad Angle in radians
* @returns {Konva.Transform}
*/
rotate(rad: number) {
const c = Math.cos(rad);
const s = Math.sin(rad);
const m11 = this.m[0] * c + this.m[2] * s;
const m12 = this.m[1] * c + this.m[3] * s;
const m21 = this.m[0] * -s + this.m[2] * c;
const m22 = this.m[1] * -s + this.m[3] * c;
this.m[0] = m11;
this.m[1] = m12;
this.m[2] = m21;
this.m[3] = m22;
return this;
}
/**
* Returns the translation
* @method
* @name Konva.Transform#getTranslation
* @returns {Object} 2D point(x, y)
*/
getTranslation() {
return {
x: this.m[4],
y: this.m[5],
};
}
/**
* Apply skew
* @method
* @name Konva.Transform#skew
* @param {Number} sx
* @param {Number} sy
* @returns {Konva.Transform}
*/
skew(sx: number, sy: number) {
const m11 = this.m[0] + this.m[2] * sy;
const m12 = this.m[1] + this.m[3] * sy;
const m21 = this.m[2] + this.m[0] * sx;
const m22 = this.m[3] + this.m[1] * sx;
this.m[0] = m11;
this.m[1] = m12;
this.m[2] = m21;
this.m[3] = m22;
return this;
}
/**
* Transform multiplication
* @method
* @name Konva.Transform#multiply
* @param {Konva.Transform} matrix
* @returns {Konva.Transform}
*/
multiply(matrix: Transform) {
const m11 = this.m[0] * matrix.m[0] + this.m[2] * matrix.m[1];
const m12 = this.m[1] * matrix.m[0] + this.m[3] * matrix.m[1];
const m21 = this.m[0] * matrix.m[2] + this.m[2] * matrix.m[3];
const m22 = this.m[1] * matrix.m[2] + this.m[3] * matrix.m[3];
const dx = this.m[0] * matrix.m[4] + this.m[2] * matrix.m[5] + this.m[4];
const dy = this.m[1] * matrix.m[4] + this.m[3] * matrix.m[5] + this.m[5];
this.m[0] = m11;
this.m[1] = m12;
this.m[2] = m21;
this.m[3] = m22;
this.m[4] = dx;
this.m[5] = dy;
return this;
}
/**
* Invert the matrix
* @method
* @name Konva.Transform#invert
* @returns {Konva.Transform}
*/
invert() {
const d = 1 / (this.m[0] * this.m[3] - this.m[1] * this.m[2]);
const m0 = this.m[3] * d;
const m1 = -this.m[1] * d;
const m2 = -this.m[2] * d;
const m3 = this.m[0] * d;
const m4 = d * (this.m[2] * this.m[5] - this.m[3] * this.m[4]);
const m5 = d * (this.m[1] * this.m[4] - this.m[0] * this.m[5]);
this.m[0] = m0;
this.m[1] = m1;
this.m[2] = m2;
this.m[3] = m3;
this.m[4] = m4;
this.m[5] = m5;
return this;
}
/**
* return matrix
* @method
* @name Konva.Transform#getMatrix
*/
getMatrix() {
return this.m;
}
/**
* convert transformation matrix back into node's attributes
* @method
* @name Konva.Transform#decompose
* @returns {Konva.Transform}
*/
decompose() {
const a = this.m[0];
const b = this.m[1];
const c = this.m[2];
const d = this.m[3];
const e = this.m[4];
const f = this.m[5];
const delta = a * d - b * c;
const result = {
x: e,
y: f,
rotation: 0,
scaleX: 0,
scaleY: 0,
skewX: 0,
skewY: 0,
};
// Apply the QR-like decomposition.
if (a != 0 || b != 0) {
const r = Math.sqrt(a * a + b * b);
result.rotation = b > 0 ? Math.acos(a / r) : -Math.acos(a / r);
result.scaleX = r;
result.scaleY = delta / r;
result.skewX = (a * c + b * d) / delta;
result.skewY = 0;
} else if (c != 0 || d != 0) {
const s = Math.sqrt(c * c + d * d);
result.rotation =
Math.PI / 2 - (d > 0 ? Math.acos(-c / s) : -Math.acos(c / s));
result.scaleX = delta / s;
result.scaleY = s;
result.skewX = 0;
result.skewY = (a * c + b * d) / delta;
} else {
// a = b = c = d = 0
}
result.rotation = Util._getRotation(result.rotation);
return result;
}
}
// CONSTANTS
const OBJECT_ARRAY = '[object Array]',
OBJECT_NUMBER = '[object Number]',
OBJECT_STRING = '[object String]',
OBJECT_BOOLEAN = '[object Boolean]',
PI_OVER_DEG180 = Math.PI / 180,
DEG180_OVER_PI = 180 / Math.PI,
HASH = '#',
EMPTY_STRING = '',
ZERO = '0',
KONVA_WARNING = 'Konva warning: ',
KONVA_ERROR = 'Konva error: ',
RGB_PAREN = 'rgb(',
COLORS = {
aliceblue: [240, 248, 255],
antiquewhite: [250, 235, 215],
aqua: [0, 255, 255],
aquamarine: [127, 255, 212],
azure: [240, 255, 255],
beige: [245, 245, 220],
bisque: [255, 228, 196],
black: [0, 0, 0],
blanchedalmond: [255, 235, 205],
blue: [0, 0, 255],
blueviolet: [138, 43, 226],
brown: [165, 42, 42],
burlywood: [222, 184, 135],
cadetblue: [95, 158, 160],
chartreuse: [127, 255, 0],
chocolate: [210, 105, 30],
coral: [255, 127, 80],
cornflowerblue: [100, 149, 237],
cornsilk: [255, 248, 220],
crimson: [220, 20, 60],
cyan: [0, 255, 255],
darkblue: [0, 0, 139],
darkcyan: [0, 139, 139],
darkgoldenrod: [184, 132, 11],
darkgray: [169, 169, 169],
darkgreen: [0, 100, 0],
darkgrey: [169, 169, 169],
darkkhaki: [189, 183, 107],
darkmagenta: [139, 0, 139],
darkolivegreen: [85, 107, 47],
darkorange: [255, 140, 0],
darkorchid: [153, 50, 204],
darkred: [139, 0, 0],
darksalmon: [233, 150, 122],
darkseagreen: [143, 188, 143],
darkslateblue: [72, 61, 139],
darkslategray: [47, 79, 79],
darkslategrey: [47, 79, 79],
darkturquoise: [0, 206, 209],
darkviolet: [148, 0, 211],
deeppink: [255, 20, 147],
deepskyblue: [0, 191, 255],
dimgray: [105, 105, 105],
dimgrey: [105, 105, 105],
dodgerblue: [30, 144, 255],
firebrick: [178, 34, 34],
floralwhite: [255, 255, 240],
forestgreen: [34, 139, 34],
fuchsia: [255, 0, 255],
gainsboro: [220, 220, 220],
ghostwhite: [248, 248, 255],
gold: [255, 215, 0],
goldenrod: [218, 165, 32],
gray: [128, 128, 128],
green: [0, 128, 0],
greenyellow: [173, 255, 47],
grey: [128, 128, 128],
honeydew: [240, 255, 240],
hotpink: [255, 105, 180],
indianred: [205, 92, 92],
indigo: [75, 0, 130],
ivory: [255, 255, 240],
khaki: [240, 230, 140],
lavender: [230, 230, 250],
lavenderblush: [255, 240, 245],
lawngreen: [124, 252, 0],
lemonchiffon: [255, 250, 205],
lightblue: [173, 216, 230],
lightcoral: [240, 128, 128],
lightcyan: [224, 255, 255],
lightgoldenrodyellow: [250, 250, 210],
lightgray: [211, 211, 211],
lightgreen: [144, 238, 144],
lightgrey: [211, 211, 211],
lightpink: [255, 182, 193],
lightsalmon: [255, 160, 122],
lightseagreen: [32, 178, 170],
lightskyblue: [135, 206, 250],
lightslategray: [119, 136, 153],
lightslategrey: [119, 136, 153],
lightsteelblue: [176, 196, 222],
lightyellow: [255, 255, 224],
lime: [0, 255, 0],
limegreen: [50, 205, 50],
linen: [250, 240, 230],
magenta: [255, 0, 255],
maroon: [128, 0, 0],
mediumaquamarine: [102, 205, 170],
mediumblue: [0, 0, 205],
mediumorchid: [186, 85, 211],
mediumpurple: [147, 112, 219],
mediumseagreen: [60, 179, 113],
mediumslateblue: [123, 104, 238],
mediumspringgreen: [0, 250, 154],
mediumturquoise: [72, 209, 204],
mediumvioletred: [199, 21, 133],
midnightblue: [25, 25, 112],
mintcream: [245, 255, 250],
mistyrose: [255, 228, 225],
moccasin: [255, 228, 181],
navajowhite: [255, 222, 173],
navy: [0, 0, 128],
oldlace: [253, 245, 230],
olive: [128, 128, 0],
olivedrab: [107, 142, 35],
orange: [255, 165, 0],
orangered: [255, 69, 0],
orchid: [218, 112, 214],
palegoldenrod: [238, 232, 170],
palegreen: [152, 251, 152],
paleturquoise: [175, 238, 238],
palevioletred: [219, 112, 147],
papayawhip: [255, 239, 213],
peachpuff: [255, 218, 185],
peru: [205, 133, 63],
pink: [255, 192, 203],
plum: [221, 160, 203],
powderblue: [176, 224, 230],
purple: [128, 0, 128],
rebeccapurple: [102, 51, 153],
red: [255, 0, 0],
rosybrown: [188, 143, 143],
royalblue: [65, 105, 225],
saddlebrown: [139, 69, 19],
salmon: [250, 128, 114],
sandybrown: [244, 164, 96],
seagreen: [46, 139, 87],
seashell: [255, 245, 238],
sienna: [160, 82, 45],
silver: [192, 192, 192],
skyblue: [135, 206, 235],
slateblue: [106, 90, 205],
slategray: [119, 128, 144],
slategrey: [119, 128, 144],
snow: [255, 255, 250],
springgreen: [0, 255, 127],
steelblue: [70, 130, 180],
tan: [210, 180, 140],
teal: [0, 128, 128],
thistle: [216, 191, 216],
transparent: [255, 255, 255, 0],
tomato: [255, 99, 71],
turquoise: [64, 224, 208],
violet: [238, 130, 238],
wheat: [245, 222, 179],
white: [255, 255, 255],
whitesmoke: [245, 245, 245],
yellow: [255, 255, 0],
yellowgreen: [154, 205, 5],
},
RGB_REGEX = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/;
let animQueue: Array = [];
// Cache for canvas farbling detection
let _isCanvasFarblingActive: boolean | null = null;
const req =
(typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame) ||
function (f) {
setTimeout(f, 16); // 60fps ≈ 16.67ms per frame
};
/**
* @namespace Util
* @memberof Konva
*/
export const Util = {
/*
* cherry-picked utilities from underscore.js
*/
_isElement(obj: any): obj is Element {
return !!(obj && obj.nodeType == 1);
},
_isFunction(obj: any) {
return !!(obj && obj.constructor && obj.call && obj.apply);
},
_isPlainObject(obj: any) {
return !!obj && obj.constructor === Object;
},
_isArray(obj: any): obj is Array {
return Object.prototype.toString.call(obj) === OBJECT_ARRAY;
},
_isNumber(obj: any): obj is number {
return (
Object.prototype.toString.call(obj) === OBJECT_NUMBER &&
!isNaN(obj) &&
isFinite(obj)
);
},
_isString(obj: any): obj is string {
return Object.prototype.toString.call(obj) === OBJECT_STRING;
},
_isBoolean(obj: any): obj is boolean {
return Object.prototype.toString.call(obj) === OBJECT_BOOLEAN;
},
// arrays are objects too
isObject(val: any): val is object {
return val instanceof Object;
},
isValidSelector(selector: any) {
if (typeof selector !== 'string') {
return false;
}
const firstChar = selector[0];
return (
firstChar === '#' ||
firstChar === '.' ||
firstChar === firstChar.toUpperCase()
);
},
_sign(number: number) {
if (number === 0) {
// that is not what sign usually returns
// but that is what we need
return 1;
}
if (number > 0) {
return 1;
} else {
return -1;
}
},
requestAnimFrame(callback: Function) {
animQueue.push(callback);
if (animQueue.length === 1) {
req(function () {
const queue = animQueue;
animQueue = [];
queue.forEach(function (cb) {
cb();
});
});
}
},
createCanvasElement() {
ensureBrowser();
const canvas = document.createElement('canvas');
// on some environments canvas.style is readonly
try {
(canvas as any).style = canvas.style || {};
} catch (e) {}
return canvas;
},
createImageElement() {
ensureBrowser();
return document.createElement('img');
},
_isInDocument(el: any) {
while ((el = el.parentNode)) {
if (el == document) {
return true;
}
}
return false;
},
/*
* arg can be an image object or image data
*/
_urlToImage(url: string, callback: Function) {
// if arg is a string, then it's a data url
const imageObj = Util.createImageElement();
imageObj.onload = function () {
callback(imageObj);
};
imageObj.src = url;
},
_rgbToHex(r: number, g: number, b: number) {
return ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
},
_hexToRgb(hex: string): RGB {
hex = hex.replace(HASH, EMPTY_STRING);
const bigint = parseInt(hex, 16);
return {
r: (bigint >> 16) & 255,
g: (bigint >> 8) & 255,
b: bigint & 255,
};
},
/**
* return random hex color
* @method
* @memberof Konva.Util
* @example
* shape.fill(Konva.Util.getRandomColor());
*/
getRandomColor() {
let randColor = ((Math.random() * 0xffffff) << 0).toString(16);
while (randColor.length < 6) {
randColor = ZERO + randColor;
}
return HASH + randColor;
},
/**
* Check if canvas farbling is active (e.g., Brave browser fingerprinting protection)
* @method
* @memberof Konva.Util
* @returns {Boolean}
*/
isCanvasFarblingActive() {
if (_isCanvasFarblingActive !== null) {
return _isCanvasFarblingActive;
}
if (typeof document === 'undefined') {
_isCanvasFarblingActive = false;
return false;
}
const c = this.createCanvasElement();
c.width = 10;
c.height = 10;
const ctx = c.getContext('2d', {
willReadFrequently: true,
}) as CanvasRenderingContext2D;
ctx.clearRect(0, 0, 10, 10);
ctx.fillStyle = '#282828'; // 40, 40, 40
ctx.fillRect(0, 0, 10, 10);
const d = ctx.getImageData(0, 0, 10, 10).data;
let isFarbling = false;
for (let i = 0; i < 100; i++) {
if (
d[i * 4] !== 40 ||
d[i * 4 + 1] !== 40 ||
d[i * 4 + 2] !== 40 ||
d[i * 4 + 3] !== 255
) {
isFarbling = true;
break;
}
}
_isCanvasFarblingActive = isFarbling;
this.releaseCanvas(c);
return _isCanvasFarblingActive;
},
/**
* Get a random color for hit detection (normalized if farbling is active)
* @method
* @memberof Konva.Util
* @returns {String} hex color string
*/
getHitColor(): string {
const color = this.getRandomColor();
return this.isCanvasFarblingActive()
? this.getSnappedHexColor(color)
: color;
},
/**
* Get hit color key from RGB values (normalized if farbling is active)
* @method
* @memberof Konva.Util
* @param {Number} r - red component (0-255)
* @param {Number} g - green component (0-255)
* @param {Number} b - blue component (0-255)
* @returns {String} hex color key string
*/
getHitColorKey(r: number, g: number, b: number): string {
if (this.isCanvasFarblingActive()) {
r = Math.round(r / 5) * 5;
g = Math.round(g / 5) * 5;
b = Math.round(b / 5) * 5;
}
return HASH + this._rgbToHex(r, g, b);
},
/**
* Snap hex color values to end with 0 (normalize for canvas farbling)
* @method
* @memberof Konva.Util
* @param {String} hex - hex color string (e.g., "#ff00ff")
* @returns {String} normalized hex color string
*/
getSnappedHexColor(hex: string): string {
const rgb = this._hexToRgb(hex);
return (
HASH +
this._rgbToHex(
Math.round(rgb.r / 5) * 5,
Math.round(rgb.g / 5) * 5,
Math.round(rgb.b / 5) * 5
)
);
},
/**
* get RGB components of a color
* @method
* @memberof Konva.Util
* @param {String} color
* @example
* // each of the following examples return {r:0, g:0, b:255}
* var rgb = Konva.Util.getRGB('blue');
* var rgb = Konva.Util.getRGB('#0000ff');
* var rgb = Konva.Util.getRGB('rgb(0,0,255)');
*/
getRGB(color: string): RGB {
let rgb;
// color string
if (color in COLORS) {
rgb = COLORS[color as keyof typeof COLORS];
return {
r: rgb[0],
g: rgb[1],
b: rgb[2],
};
} else if (color[0] === HASH) {
// hex
return this._hexToRgb(color.substring(1));
} else if (color.substr(0, 4) === RGB_PAREN) {
// rgb string
rgb = RGB_REGEX.exec(color.replace(/ /g, '')) as RegExpExecArray;
return {
r: parseInt(rgb[1], 10),
g: parseInt(rgb[2], 10),
b: parseInt(rgb[3], 10),
};
} else {
// default
return {
r: 0,
g: 0,
b: 0,
};
}
},
// convert any color string to RGBA object
// from https://github.com/component/color-parser
colorToRGBA(str: string) {
str = str || 'black';
return (
Util._namedColorToRBA(str) ||
Util._hex3ColorToRGBA(str) ||
Util._hex4ColorToRGBA(str) ||
Util._hex6ColorToRGBA(str) ||
Util._hex8ColorToRGBA(str) ||
Util._rgbColorToRGBA(str) ||
Util._rgbaColorToRGBA(str) ||
Util._hslColorToRGBA(str)
);
},
// Parse named css color. Like "green"
_namedColorToRBA(str: string) {
const c = COLORS[str.toLowerCase() as keyof typeof COLORS];
if (!c) {
return null;
}
return {
r: c[0],
g: c[1],
b: c[2],
a: 1,
};
},
// Parse rgb(n, n, n)
_rgbColorToRGBA(str: string) {
if (str.indexOf('rgb(') === 0) {
str = str.match(/rgb\(([^)]+)\)/)![1];
const parts = str.split(/ *, */).map(Number);
return {
r: parts[0],
g: parts[1],
b: parts[2],
a: 1,
};
}
},
// Parse rgba(n, n, n, n)
_rgbaColorToRGBA(str: string) {
if (str.indexOf('rgba(') === 0) {
str = str.match(/rgba\(([^)]+)\)/)![1]!;
const parts = str.split(/ *, */).map((n, index) => {
if (n.slice(-1) === '%') {
return index === 3 ? parseInt(n) / 100 : (parseInt(n) / 100) * 255;
}
return Number(n);
});
return {
r: parts[0],
g: parts[1],
b: parts[2],
a: parts[3],
};
}
},
// Parse #nnnnnnnn
_hex8ColorToRGBA(str: string) {
if (str[0] === '#' && str.length === 9) {
return {
r: parseInt(str.slice(1, 3), 16),
g: parseInt(str.slice(3, 5), 16),
b: parseInt(str.slice(5, 7), 16),
a: parseInt(str.slice(7, 9), 16) / 0xff,
};
}
},
// Parse #nnnnnn
_hex6ColorToRGBA(str: string) {
if (str[0] === '#' && str.length === 7) {
return {
r: parseInt(str.slice(1, 3), 16),
g: parseInt(str.slice(3, 5), 16),
b: parseInt(str.slice(5, 7), 16),
a: 1,
};
}
},
// Parse #nnnn
_hex4ColorToRGBA(str: string) {
if (str[0] === '#' && str.length === 5) {
return {
r: parseInt(str[1] + str[1], 16),
g: parseInt(str[2] + str[2], 16),
b: parseInt(str[3] + str[3], 16),
a: parseInt(str[4] + str[4], 16) / 0xff,
};
}
},
// Parse #nnn
_hex3ColorToRGBA(str: string) {
if (str[0] === '#' && str.length === 4) {
return {
r: parseInt(str[1] + str[1], 16),
g: parseInt(str[2] + str[2], 16),
b: parseInt(str[3] + str[3], 16),
a: 1,
};
}
},
// Code adapted from https://github.com/Qix-/color-convert/blob/master/conversions.js#L244
_hslColorToRGBA(str: string) {
// Check hsl() format
if (/hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.test(str)) {
// Extract h, s, l
const [_, ...hsl] = /hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.exec(str)!;
const h = Number(hsl[0]) / 360;
const s = Number(hsl[1]) / 100;
const l = Number(hsl[2]) / 100;
let t2;
let t3;
let val;
if (s === 0) {
val = l * 255;
return {
r: Math.round(val),
g: Math.round(val),
b: Math.round(val),
a: 1,
};
}
if (l < 0.5) {
t2 = l * (1 + s);
} else {
t2 = l + s - l * s;
}
const t1 = 2 * l - t2;
const rgb = [0, 0, 0];
for (let i = 0; i < 3; i++) {
t3 = h + (1 / 3) * -(i - 1);
if (t3 < 0) {
t3++;
}
if (t3 > 1) {
t3--;
}
if (6 * t3 < 1) {
val = t1 + (t2 - t1) * 6 * t3;
} else if (2 * t3 < 1) {
val = t2;
} else if (3 * t3 < 2) {
val = t1 + (t2 - t1) * (2 / 3 - t3) * 6;
} else {
val = t1;
}
rgb[i] = val * 255;
}
return {
r: Math.round(rgb[0]),
g: Math.round(rgb[1]),
b: Math.round(rgb[2]),
a: 1,
};
}
},
/**
* check intersection of two client rectangles
* @method
* @memberof Konva.Util
* @param {Object} r1 - { x, y, width, height } client rectangle
* @param {Object} r2 - { x, y, width, height } client rectangle
* @example
* const overlapping = Konva.Util.haveIntersection(shape1.getClientRect(), shape2.getClientRect());
*/
haveIntersection(r1: IRect, r2: IRect) {
return !(
r2.x > r1.x + r1.width ||
r2.x + r2.width < r1.x ||
r2.y > r1.y + r1.height ||
r2.y + r2.height < r1.y
);
},
cloneObject(obj: Any): Any {
const retObj: any = {};
for (const key in obj) {
if (this._isPlainObject(obj[key])) {
retObj[key] = this.cloneObject(obj[key]);
} else if (this._isArray(obj[key])) {
retObj[key] = this.cloneArray(obj[key] as Array);
} else {
retObj[key] = obj[key];
}
}
return retObj;
},
cloneArray(arr: Array) {
return arr.slice(0);
},
degToRad(deg: number) {
return deg * PI_OVER_DEG180;
},
radToDeg(rad: number) {
return rad * DEG180_OVER_PI;
},
_degToRad(deg: number) {
Util.warn(
'Util._degToRad is removed. Please use public Util.degToRad instead.'
);
return Util.degToRad(deg);
},
_radToDeg(rad: number) {
Util.warn(
'Util._radToDeg is removed. Please use public Util.radToDeg instead.'
);
return Util.radToDeg(rad);
},
_getRotation(radians: number) {
return Konva.angleDeg ? Util.radToDeg(radians) : radians;
},
_capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
},
throw(str: string) {
throw new Error(KONVA_ERROR + str);
},
error(str: string) {
console.error(KONVA_ERROR + str);
},
warn(str: string) {
if (!Konva.showWarnings) {
return;
}
console.warn(KONVA_WARNING + str);
},
each(obj: object, func: Function) {
for (const key in obj) {
func(key, obj[key as keyof typeof obj]);
}
},
_inRange(val: number, left: number, right: number) {
return left <= val && val < right;
},
_getProjectionToSegment(x1, y1, x2, y2, x3, y3) {
let x, y, dist;
const pd2 = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
if (pd2 == 0) {
x = x1;
y = y1;
dist = (x3 - x2) * (x3 - x2) + (y3 - y2) * (y3 - y2);
} else {
const u = ((x3 - x1) * (x2 - x1) + (y3 - y1) * (y2 - y1)) / pd2;
if (u < 0) {
x = x1;
y = y1;
dist = (x1 - x3) * (x1 - x3) + (y1 - y3) * (y1 - y3);
} else if (u > 1.0) {
x = x2;
y = y2;
dist = (x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3);
} else {
x = x1 + u * (x2 - x1);
y = y1 + u * (y2 - y1);
dist = (x - x3) * (x - x3) + (y - y3) * (y - y3);
}
}
return [x, y, dist];
},
// line as array of points.
// line might be closed
_getProjectionToLine(pt: Vector2d, line: Array, isClosed: boolean) {
const pc = Util.cloneObject(pt);
let dist = Number.MAX_VALUE;
line.forEach(function (p1, i) {
if (!isClosed && i === line.length - 1) {
return;
}
const p2 = line[(i + 1) % line.length];
const proj = Util._getProjectionToSegment(
p1.x,
p1.y,
p2.x,
p2.y,
pt.x,
pt.y
);
const px = proj[0],
py = proj[1],
pdist = proj[2];
if (pdist < dist) {
pc.x = px;
pc.y = py;
dist = pdist;
}
});
return pc;
},
_prepareArrayForTween(startArray, endArray, isClosed) {
const start: Vector2d[] = [],
end: Vector2d[] = [];
if (startArray.length > endArray.length) {
const temp = endArray;
endArray = startArray;
startArray = temp;
}
for (let n = 0; n < startArray.length; n += 2) {
start.push({
x: startArray[n],
y: startArray[n + 1],
});
}
for (let n = 0; n < endArray.length; n += 2) {
end.push({
x: endArray[n],
y: endArray[n + 1],
});
}
const newStart: number[] = [];
end.forEach(function (point) {
const pr = Util._getProjectionToLine(point, start, isClosed);
newStart.push(pr.x);
newStart.push(pr.y);
});
return newStart;
},
_prepareToStringify(obj: any): T | null {
let desc;
obj.visitedByCircularReferenceRemoval = true;
for (const key in obj) {
if (
!(obj.hasOwnProperty(key) && obj[key] && typeof obj[key] == 'object')
) {
continue;
}
desc = Object.getOwnPropertyDescriptor(obj, key);
if (
obj[key].visitedByCircularReferenceRemoval ||
Util._isElement(obj[key])
) {
if (desc.configurable) {
delete obj[key];
} else {
return null;
}
} else if (Util._prepareToStringify(obj[key]) === null) {
if (desc.configurable) {
delete obj[key];
} else {
return null;
}
}
}
delete obj.visitedByCircularReferenceRemoval;
return obj;
},
// very simplified version of Object.assign
_assign(target: T, source: U) {
for (const key in source) {
(target as any)[key] = source[key];
}
return target as T & U;
},
_getFirstPointerId(evt) {
if (!evt.touches) {
// try to use pointer id or fake id
return evt.pointerId || 999;
} else {
return evt.changedTouches[0].identifier;
}
},
releaseCanvas(...canvases: HTMLCanvasElement[]) {
if (!Konva.releaseCanvasOnDestroy) return;
canvases.forEach((c) => {
c.width = 0;
c.height = 0;
});
},
drawRoundedRectPath(
context: Context,
width: number,
height: number,
cornerRadius: number | number[]
) {
// if negative dimensions, abs width/height and move rectangle
let xOrigin = width < 0 ? width : 0;
let yOrigin = height < 0 ? height : 0;
width = Math.abs(width);
height = Math.abs(height);
let topLeft = 0;
let topRight = 0;
let bottomLeft = 0;
let bottomRight = 0;
if (typeof cornerRadius === 'number') {
topLeft =
topRight =
bottomLeft =
bottomRight =
Math.min(cornerRadius, width / 2, height / 2);
} else {
topLeft = Math.min(cornerRadius[0] || 0, width / 2, height / 2);
topRight = Math.min(cornerRadius[1] || 0, width / 2, height / 2);
bottomRight = Math.min(cornerRadius[2] || 0, width / 2, height / 2);
bottomLeft = Math.min(cornerRadius[3] || 0, width / 2, height / 2);
}
context.moveTo(xOrigin + topLeft, yOrigin);
context.lineTo(xOrigin + width - topRight, yOrigin);
context.arc(
xOrigin + width - topRight,
yOrigin + topRight,
topRight,
(Math.PI * 3) / 2,
0,
false
);
context.lineTo(xOrigin + width, yOrigin + height - bottomRight);
context.arc(
xOrigin + width - bottomRight,
yOrigin + height - bottomRight,
bottomRight,
0,
Math.PI / 2,
false
);
context.lineTo(xOrigin + bottomLeft, yOrigin + height);
context.arc(
xOrigin + bottomLeft,
yOrigin + height - bottomLeft,
bottomLeft,
Math.PI / 2,
Math.PI,
false
);
context.lineTo(xOrigin, yOrigin + topLeft);
context.arc(
xOrigin + topLeft,
yOrigin + topLeft,
topLeft,
Math.PI,
(Math.PI * 3) / 2,
false
);
},
drawRoundedPolygonPath(
context: Context,
points: Vector2d[],
sides: number,
radius: number,
cornerRadius: number | number[]
) {
radius = Math.abs(radius);
for (let i = 0; i < sides; i++) {
const prev = points[(i - 1 + sides) % sides];
const curr = points[i];
const next = points[(i + 1) % sides];
const vec1 = { x: curr.x - prev.x, y: curr.y - prev.y };
const vec2 = { x: next.x - curr.x, y: next.y - curr.y };
const len1 = Math.hypot(vec1.x, vec1.y);
const len2 = Math.hypot(vec2.x, vec2.y);
let currCornerRadius;
if (typeof cornerRadius === 'number') {
currCornerRadius = cornerRadius;
} else {
currCornerRadius = i < cornerRadius.length ? cornerRadius[i] : 0;
}
const maxCornerRadius = radius * Math.cos(Math.PI / sides);
// cornerRadius creates perfect circle at 1/2 radius
currCornerRadius =
maxCornerRadius * Math.min(1, (currCornerRadius / radius) * 2);
const normalVec1 = { x: vec1.x / len1, y: vec1.y / len1 };
const normalVec2 = { x: vec2.x / len2, y: vec2.y / len2 };
const p1 = {
x: curr.x - normalVec1.x * currCornerRadius,
y: curr.y - normalVec1.y * currCornerRadius,
};
const p2 = {
x: curr.x + normalVec2.x * currCornerRadius,
y: curr.y + normalVec2.y * currCornerRadius,
};
if (i === 0) {
context.moveTo(p1.x, p1.y);
} else {
context.lineTo(p1.x, p1.y);
}
context.arcTo(curr.x, curr.y, p2.x, p2.y, currCornerRadius);
}
},
};
export type AnyString = T | (string & {});
================================================
FILE: src/Validators.ts
================================================
import { Konva } from './Global.ts';
import { Util } from './Util.ts';
function _formatValue(val: any) {
if (Util._isString(val)) {
return '"' + val + '"';
}
if (Object.prototype.toString.call(val) === '[object Number]') {
return val;
}
if (Util._isBoolean(val)) {
return val;
}
return Object.prototype.toString.call(val);
}
export function RGBComponent(val: number) {
if (val > 255) {
return 255;
} else if (val < 0) {
return 0;
}
return Math.round(val);
}
export function alphaComponent(val: number) {
if (val > 1) {
return 1;
} else if (val < 0.0001) {
// chrome does not honor alpha values of 0
return 0.0001;
}
return val;
}
export function getNumberValidator() {
if (Konva.isUnminified) {
return function (val: T, attr: string): T {
if (!Util._isNumber(val)) {
Util.warn(
_formatValue(val) +
' is a not valid value for "' +
attr +
'" attribute. The value should be a number.'
);
}
return val;
};
}
}
export function getNumberOrArrayOfNumbersValidator(noOfElements: number) {
if (Konva.isUnminified) {
return function (val: T, attr: string): T {
let isNumber = Util._isNumber(val);
let isValidArray = Util._isArray(val) && val.length == noOfElements;
if (!isNumber && !isValidArray) {
Util.warn(
_formatValue(val) +
' is a not valid value for "' +
attr +
'" attribute. The value should be a number or Array(' +
noOfElements +
')'
);
}
return val;
};
}
}
export function getNumberOrAutoValidator() {
if (Konva.isUnminified) {
return function (val: T, attr: string): T {
const isNumber = Util._isNumber(val);
const isAuto = val === 'auto';
if (!(isNumber || isAuto)) {
Util.warn(
_formatValue(val) +
' is a not valid value for "' +
attr +
'" attribute. The value should be a number or "auto".'
);
}
return val;
};
}
}
export function getStringValidator() {
if (Konva.isUnminified) {
return function (val: T, attr: string): T {
if (!Util._isString(val)) {
Util.warn(
_formatValue(val) +
' is a not valid value for "' +
attr +
'" attribute. The value should be a string.'
);
}
return val;
};
}
}
export function getStringOrGradientValidator() {
if (Konva.isUnminified) {
return function (val: T, attr: string): T {
const isString = Util._isString(val);
const isGradient =
Object.prototype.toString.call(val) === '[object CanvasGradient]' ||
(val && val['addColorStop']);
if (!(isString || isGradient)) {
Util.warn(
_formatValue(val) +
' is a not valid value for "' +
attr +
'" attribute. The value should be a string or a native gradient.'
);
}
return val;
};
}
}
export function getFunctionValidator() {
if (Konva.isUnminified) {
return function (val: T, attr: string): T {
if (!Util._isFunction(val)) {
Util.warn(
_formatValue(val) +
' is a not valid value for "' +
attr +
'" attribute. The value should be a function.'
);
}
return val;
};
}
}
export function getNumberArrayValidator() {
if (Konva.isUnminified) {
return function (val: T, attr: string): T {
// Retrieve TypedArray constructor as found in MDN (if TypedArray is available)
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#description
const TypedArray = Int8Array ? Object.getPrototypeOf(Int8Array) : null;
if (TypedArray && val instanceof TypedArray) {
return val;
}
if (!Util._isArray(val)) {
Util.warn(
_formatValue(val) +
' is a not valid value for "' +
attr +
'" attribute. The value should be a array of numbers.'
);
} else {
val.forEach(function (item: any) {
if (!Util._isNumber(item)) {
Util.warn(
'"' +
attr +
'" attribute has non numeric element ' +
item +
'. Make sure that all elements are numbers.'
);
}
});
}
return val;
};
}
}
export function getBooleanValidator() {
if (Konva.isUnminified) {
return function (val: T, attr: string): T {
const isBool = val === true || val === false;
if (!isBool) {
Util.warn(
_formatValue(val) +
' is a not valid value for "' +
attr +
'" attribute. The value should be a boolean.'
);
}
return val;
};
}
}
export function getComponentValidator(components: string[]) {
if (Konva.isUnminified) {
return function (val: T, attr: string): T {
// ignore validation on undefined value, because it will reset to defalt
if (val === undefined || val === null) {
return val;
}
if (!Util.isObject(val)) {
Util.warn(
_formatValue(val) +
' is a not valid value for "' +
attr +
'" attribute. The value should be an object with properties ' +
components
);
}
return val;
};
}
}
================================================
FILE: src/_CoreInternals.ts
================================================
// what is core parts of Konva?
import { Konva as Global } from './Global.ts';
import { Util, Transform } from './Util.ts';
import { Node } from './Node.ts';
import { Container } from './Container.ts';
import { Stage, stages } from './Stage.ts';
import { Layer } from './Layer.ts';
import { FastLayer } from './FastLayer.ts';
import { Group } from './Group.ts';
import { DD } from './DragAndDrop.ts';
import { Shape, shapes } from './Shape.ts';
import { Animation } from './Animation.ts';
import { Tween, Easings } from './Tween.ts';
import { Context } from './Context.ts';
import { Canvas } from './Canvas.ts';
export const Konva = Util._assign(Global, {
Util,
Transform,
Node,
Container,
Stage,
stages,
Layer,
FastLayer,
Group,
DD,
Shape,
shapes,
Animation,
Tween,
Easings,
Context,
Canvas,
});
export namespace Konva {
export type Vector2d = import('./types.ts').Vector2d;
export type Node = import('./Node.ts').Node;
export type NodeConfig = import('./Node.ts').NodeConfig;
export type KonvaEventObject =
import('./Node.ts').KonvaEventObject;
export type KonvaPointerEvent =
import('./PointerEvents.ts').KonvaPointerEvent;
export type KonvaEventListener =
import('./Node.ts').KonvaEventListener;
export type Container = import('./Container.ts').Container;
export type ContainerConfig = import('./Container.ts').ContainerConfig;
export type Transform = import('./Util.ts').Transform;
export type Context = import('./Context.ts').Context;
export type Stage = import('./Stage.ts').Stage;
export type StageConfig = import('./Stage.ts').StageConfig;
export type Layer = import('./Layer.ts').Layer;
export type LayerConfig = import('./Layer.ts').LayerConfig;
export type FastLayer = import('./FastLayer.ts').FastLayer;
export type Group = import('./Group.ts').Group;
export type GroupConfig = import('./Group.ts').GroupConfig;
export type Shape = import('./Shape.ts').Shape;
export type ShapeConfig = import('./Shape.ts').ShapeConfig;
export type Animation = import('./Animation.ts').Animation;
export type Tween = import('./Tween.ts').Tween;
export type TweenConfig = import('./Tween.ts').TweenConfig;
}
export default Konva;
================================================
FILE: src/_FullInternals.ts
================================================
// we need to import core of the Konva and then extend it with all additional objects
import { Konva as Core } from './_CoreInternals.ts';
// shapes
import { Arc } from './shapes/Arc.ts';
import { Arrow } from './shapes/Arrow.ts';
import { Circle } from './shapes/Circle.ts';
import { Ellipse } from './shapes/Ellipse.ts';
import { Image } from './shapes/Image.ts';
import { Label, Tag } from './shapes/Label.ts';
import { Line } from './shapes/Line.ts';
import { Path } from './shapes/Path.ts';
import { Rect } from './shapes/Rect.ts';
import { RegularPolygon } from './shapes/RegularPolygon.ts';
import { Ring } from './shapes/Ring.ts';
import { Sprite } from './shapes/Sprite.ts';
import { Star } from './shapes/Star.ts';
import { Text } from './shapes/Text.ts';
import { TextPath } from './shapes/TextPath.ts';
import { Transformer } from './shapes/Transformer.ts';
import { Wedge } from './shapes/Wedge.ts';
// filters
import { Blur } from './filters/Blur.ts';
import { Brighten } from './filters/Brighten.ts';
import { Brightness } from './filters/Brightness.ts';
import { Contrast } from './filters/Contrast.ts';
import { Emboss } from './filters/Emboss.ts';
import { Enhance } from './filters/Enhance.ts';
import { Grayscale } from './filters/Grayscale.ts';
import { HSL } from './filters/HSL.ts';
import { HSV } from './filters/HSV.ts';
import { Invert } from './filters/Invert.ts';
import { Kaleidoscope } from './filters/Kaleidoscope.ts';
import { Mask } from './filters/Mask.ts';
import { Noise } from './filters/Noise.ts';
import { Pixelate } from './filters/Pixelate.ts';
import { Posterize } from './filters/Posterize.ts';
import { RGB } from './filters/RGB.ts';
import { RGBA } from './filters/RGBA.ts';
import { Sepia } from './filters/Sepia.ts';
import { Solarize } from './filters/Solarize.ts';
import { Threshold } from './filters/Threshold.ts';
export const Konva = Core.Util._assign(Core, {
Arc,
Arrow,
Circle,
Ellipse,
Image,
Label,
Tag,
Line,
Path,
Rect,
RegularPolygon,
Ring,
Sprite,
Star,
Text,
TextPath,
Transformer,
Wedge,
/**
* @namespace Filters
* @memberof Konva
*/
Filters: {
Blur,
Brightness,
Brighten,
Contrast,
Emboss,
Enhance,
Grayscale,
HSL,
HSV,
Invert,
Kaleidoscope,
Mask,
Noise,
Pixelate,
Posterize,
RGB,
RGBA,
Sepia,
Solarize,
Threshold,
},
});
export namespace Konva {
export type Vector2d = Core.Vector2d;
export type Node = Core.Node;
export type NodeConfig = Core.NodeConfig;
export type KonvaEventObject = Core.KonvaEventObject;
export type KonvaPointerEvent = Core.KonvaPointerEvent;
export type KonvaEventListener = Core.KonvaEventListener<
This,
EventType
>;
export type Container = Core.Container;
export type ContainerConfig = Core.ContainerConfig;
export type Transform = Core.Transform;
export type Context = Core.Context;
export type Stage = Core.Stage;
export type StageConfig = Core.StageConfig;
export type Layer = Core.Layer;
export type LayerConfig = Core.LayerConfig;
export type FastLayer = Core.FastLayer;
export type Group = Core.Group;
export type GroupConfig = Core.GroupConfig;
export type Shape = Core.Shape;
export type ShapeConfig = Core.ShapeConfig;
export type Animation = Core.Animation;
export type Tween = Core.Tween;
export type TweenConfig = Core.TweenConfig;
export type Arc = import('./shapes/Arc.ts').Arc;
export type ArcConfig = import('./shapes/Arc.ts').ArcConfig;
export type Arrow = import('./shapes/Arrow.ts').Arrow;
export type ArrowConfig = import('./shapes/Arrow.ts').ArrowConfig;
export type Circle = import('./shapes/Circle.ts').Circle;
export type CircleConfig = import('./shapes/Circle.ts').CircleConfig;
export type Ellipse = import('./shapes/Ellipse.ts').Ellipse;
export type EllipseConfig = import('./shapes/Ellipse.ts').EllipseConfig;
export type Image = import('./shapes/Image.ts').Image;
export type ImageConfig = import('./shapes/Image.ts').ImageConfig;
export type Label = import('./shapes/Label.ts').Label;
export type LabelConfig = import('./shapes/Label.ts').LabelConfig;
export type Tag = import('./shapes/Label.ts').Tag;
export type TagConfig = import('./shapes/Label.ts').TagConfig;
export type Line = import('./shapes/Line.ts').Line;
export type LineConfig = import('./shapes/Line.ts').LineConfig;
export type Path = import('./shapes/Path.ts').Path;
export type PathConfig = import('./shapes/Path.ts').PathConfig;
export type Rect = import('./shapes/Rect.ts').Rect;
export type RectConfig = import('./shapes/Rect.ts').RectConfig;
export type RegularPolygon =
import('./shapes/RegularPolygon.ts').RegularPolygon;
export type RegularPolygonConfig =
import('./shapes/RegularPolygon.ts').RegularPolygonConfig;
export type Ring = import('./shapes/Ring.ts').Ring;
export type RingConfig = import('./shapes/Ring.ts').RingConfig;
export type Sprite = import('./shapes/Sprite.ts').Sprite;
export type SpriteConfig = import('./shapes/Sprite.ts').SpriteConfig;
export type Star = import('./shapes/Star.ts').Star;
export type StarConfig = import('./shapes/Star.ts').StarConfig;
export type Text = import('./shapes/Text.ts').Text;
export type TextConfig = import('./shapes/Text.ts').TextConfig;
export type TextPath = import('./shapes/TextPath.ts').TextPath;
export type TextPathConfig = import('./shapes/TextPath.ts').TextPathConfig;
export type Transformer = import('./shapes/Transformer.ts').Transformer;
export type TransformerConfig =
import('./shapes/Transformer.ts').TransformerConfig;
export type Wedge = import('./shapes/Wedge.ts').Wedge;
export type WedgeConfig = import('./shapes/Wedge.ts').WedgeConfig;
}
================================================
FILE: src/canvas-backend.ts
================================================
import { Konva } from './_CoreInternals.ts';
// @ts-ignore
import * as Canvas from 'canvas';
const canvas = Canvas['default'] || Canvas;
// @ts-ignore
global.DOMMatrix = canvas.DOMMatrix;
// @ts-ignore
(global as any).Path2D ??= class Path2D {
constructor(path: any) {
(this as any).path = path;
}
get [Symbol.toStringTag]() {
return `Path2D`;
}
};
Konva.Util['createCanvasElement'] = () => {
const node = canvas.createCanvas(300, 300) as any;
if (!node['style']) {
node['style'] = {};
}
return node;
};
// create image in Node env
Konva.Util.createImageElement = () => {
const node = new canvas.Image() as any;
return node;
};
Konva._renderBackend = 'node-canvas';
export default Konva;
================================================
FILE: src/filters/Blur.ts
================================================
import { Factory } from '../Factory.ts';
import type { Filter } from '../Node.ts';
import { Node } from '../Node.ts';
import { getNumberValidator } from '../Validators.ts';
/*
the Gauss filter
master repo: https://github.com/pavelpower/kineticjsGaussFilter
*/
/*
StackBlur - a fast almost Gaussian Blur For Canvas
Version: 0.5
Author: Mario Klingemann
Contact: mario@quasimondo.com
Website: http://www.quasimondo.com/StackBlurForCanvas
Twitter: @quasimondo
In case you find this class useful - especially in commercial projects -
I am not totally unhappy for a small donation to my PayPal account
mario@quasimondo.de
Or support me on flattr:
https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript
Copyright (c) 2010 Mario Klingemann
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.
* @license MIT
*/
function BlurStack(this: any) {
this.r = 0;
this.g = 0;
this.b = 0;
this.a = 0;
this.next = null;
}
const mul_table = [
512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292,
512, 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292,
273, 512, 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259,
496, 475, 456, 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292,
282, 273, 265, 512, 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373,
364, 354, 345, 337, 328, 320, 312, 305, 298, 291, 284, 278, 271, 265, 259,
507, 496, 485, 475, 465, 456, 446, 437, 428, 420, 412, 404, 396, 388, 381,
374, 367, 360, 354, 347, 341, 335, 329, 323, 318, 312, 307, 302, 297, 292,
287, 282, 278, 273, 269, 265, 261, 512, 505, 497, 489, 482, 475, 468, 461,
454, 447, 441, 435, 428, 422, 417, 411, 405, 399, 394, 389, 383, 378, 373,
368, 364, 359, 354, 350, 345, 341, 337, 332, 328, 324, 320, 316, 312, 309,
305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, 268, 265, 262, 259,
257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, 451, 446, 442,
437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, 385, 381,
377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, 332,
329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292,
289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259,
];
const shg_table = [
9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 17, 17,
17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19,
19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23,
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24,
];
function filterGaussBlurRGBA(imageData, radius) {
const pixels = imageData.data,
width = imageData.width,
height = imageData.height;
let p,
yi,
yw,
r_sum,
g_sum,
b_sum,
a_sum,
r_out_sum,
g_out_sum,
b_out_sum,
a_out_sum,
r_in_sum,
g_in_sum,
b_in_sum,
a_in_sum,
pr,
pg,
pb,
pa,
rbs;
const div = radius + radius + 1,
widthMinus1 = width - 1,
heightMinus1 = height - 1,
radiusPlus1 = radius + 1,
sumFactor = (radiusPlus1 * (radiusPlus1 + 1)) / 2,
stackStart = new BlurStack(),
mul_sum = mul_table[radius],
shg_sum = shg_table[radius];
let stackEnd = null,
stack = stackStart,
stackIn: any = null,
stackOut: any = null;
for (let i = 1; i < div; i++) {
stack = stack.next = new BlurStack();
if (i === radiusPlus1) {
stackEnd = stack;
}
}
stack.next = stackStart;
yw = yi = 0;
for (let y = 0; y < height; y++) {
r_in_sum =
g_in_sum =
b_in_sum =
a_in_sum =
r_sum =
g_sum =
b_sum =
a_sum =
0;
r_out_sum = radiusPlus1 * (pr = pixels[yi]);
g_out_sum = radiusPlus1 * (pg = pixels[yi + 1]);
b_out_sum = radiusPlus1 * (pb = pixels[yi + 2]);
a_out_sum = radiusPlus1 * (pa = pixels[yi + 3]);
r_sum += sumFactor * pr;
g_sum += sumFactor * pg;
b_sum += sumFactor * pb;
a_sum += sumFactor * pa;
stack = stackStart;
for (let i = 0; i < radiusPlus1; i++) {
stack.r = pr;
stack.g = pg;
stack.b = pb;
stack.a = pa;
stack = stack.next;
}
for (let i = 1; i < radiusPlus1; i++) {
p = yi + ((widthMinus1 < i ? widthMinus1 : i) << 2);
r_sum += (stack.r = pr = pixels[p]) * (rbs = radiusPlus1 - i);
g_sum += (stack.g = pg = pixels[p + 1]) * rbs;
b_sum += (stack.b = pb = pixels[p + 2]) * rbs;
a_sum += (stack.a = pa = pixels[p + 3]) * rbs;
r_in_sum += pr;
g_in_sum += pg;
b_in_sum += pb;
a_in_sum += pa;
stack = stack.next;
}
stackIn = stackStart;
stackOut = stackEnd;
for (let x = 0; x < width; x++) {
pixels[yi + 3] = pa = (a_sum * mul_sum) >> shg_sum;
if (pa !== 0) {
pa = 255 / pa;
pixels[yi] = ((r_sum * mul_sum) >> shg_sum) * pa;
pixels[yi + 1] = ((g_sum * mul_sum) >> shg_sum) * pa;
pixels[yi + 2] = ((b_sum * mul_sum) >> shg_sum) * pa;
} else {
pixels[yi] = pixels[yi + 1] = pixels[yi + 2] = 0;
}
r_sum -= r_out_sum;
g_sum -= g_out_sum;
b_sum -= b_out_sum;
a_sum -= a_out_sum;
r_out_sum -= stackIn.r;
g_out_sum -= stackIn.g;
b_out_sum -= stackIn.b;
a_out_sum -= stackIn.a;
p = (yw + ((p = x + radius + 1) < widthMinus1 ? p : widthMinus1)) << 2;
r_in_sum += stackIn.r = pixels[p];
g_in_sum += stackIn.g = pixels[p + 1];
b_in_sum += stackIn.b = pixels[p + 2];
a_in_sum += stackIn.a = pixels[p + 3];
r_sum += r_in_sum;
g_sum += g_in_sum;
b_sum += b_in_sum;
a_sum += a_in_sum;
stackIn = stackIn.next;
r_out_sum += pr = stackOut.r;
g_out_sum += pg = stackOut.g;
b_out_sum += pb = stackOut.b;
a_out_sum += pa = stackOut.a;
r_in_sum -= pr;
g_in_sum -= pg;
b_in_sum -= pb;
a_in_sum -= pa;
stackOut = stackOut.next;
yi += 4;
}
yw += width;
}
for (let x = 0; x < width; x++) {
g_in_sum =
b_in_sum =
a_in_sum =
r_in_sum =
g_sum =
b_sum =
a_sum =
r_sum =
0;
yi = x << 2;
r_out_sum = radiusPlus1 * (pr = pixels[yi]);
g_out_sum = radiusPlus1 * (pg = pixels[yi + 1]);
b_out_sum = radiusPlus1 * (pb = pixels[yi + 2]);
a_out_sum = radiusPlus1 * (pa = pixels[yi + 3]);
r_sum += sumFactor * pr;
g_sum += sumFactor * pg;
b_sum += sumFactor * pb;
a_sum += sumFactor * pa;
stack = stackStart;
for (let i = 0; i < radiusPlus1; i++) {
stack.r = pr;
stack.g = pg;
stack.b = pb;
stack.a = pa;
stack = stack.next;
}
let yp = width;
for (let i = 1; i <= radius; i++) {
yi = (yp + x) << 2;
r_sum += (stack.r = pr = pixels[yi]) * (rbs = radiusPlus1 - i);
g_sum += (stack.g = pg = pixels[yi + 1]) * rbs;
b_sum += (stack.b = pb = pixels[yi + 2]) * rbs;
a_sum += (stack.a = pa = pixels[yi + 3]) * rbs;
r_in_sum += pr;
g_in_sum += pg;
b_in_sum += pb;
a_in_sum += pa;
stack = stack.next;
if (i < heightMinus1) {
yp += width;
}
}
yi = x;
stackIn = stackStart;
stackOut = stackEnd;
for (let y = 0; y < height; y++) {
p = yi << 2;
pixels[p + 3] = pa = (a_sum * mul_sum) >> shg_sum;
if (pa > 0) {
pa = 255 / pa;
pixels[p] = ((r_sum * mul_sum) >> shg_sum) * pa;
pixels[p + 1] = ((g_sum * mul_sum) >> shg_sum) * pa;
pixels[p + 2] = ((b_sum * mul_sum) >> shg_sum) * pa;
} else {
pixels[p] = pixels[p + 1] = pixels[p + 2] = 0;
}
r_sum -= r_out_sum;
g_sum -= g_out_sum;
b_sum -= b_out_sum;
a_sum -= a_out_sum;
r_out_sum -= stackIn.r;
g_out_sum -= stackIn.g;
b_out_sum -= stackIn.b;
a_out_sum -= stackIn.a;
p =
(x +
((p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1) * width) <<
2;
r_sum += r_in_sum += stackIn.r = pixels[p];
g_sum += g_in_sum += stackIn.g = pixels[p + 1];
b_sum += b_in_sum += stackIn.b = pixels[p + 2];
a_sum += a_in_sum += stackIn.a = pixels[p + 3];
stackIn = stackIn.next;
r_out_sum += pr = stackOut.r;
g_out_sum += pg = stackOut.g;
b_out_sum += pb = stackOut.b;
a_out_sum += pa = stackOut.a;
r_in_sum -= pr;
g_in_sum -= pg;
b_in_sum -= pb;
a_in_sum -= pa;
stackOut = stackOut.next;
yi += width;
}
}
}
/**
* Blur Filter
* @function
* @name Blur
* @memberof Konva.Filters
* @param {Object} imageData
* @example
* node.cache();
* node.filters([Konva.Filters.Blur]);
* node.blurRadius(10);
*/
export const Blur: Filter = function Blur(imageData) {
const radius = Math.round(this.blurRadius());
if (radius > 0) {
filterGaussBlurRGBA(imageData, radius);
}
};
Factory.addGetterSetter(
Node,
'blurRadius',
0,
getNumberValidator(),
Factory.afterSetFilter
);
/**
* get/set blur radius. Use with {@link Konva.Filters.Blur} filter
* @name Konva.Node#blurRadius
* @method
* @param {Integer} radius
* @returns {Integer}
*/
================================================
FILE: src/filters/Brighten.ts
================================================
import { Factory } from '../Factory.ts';
import type { Filter } from '../Node.ts';
import { Node } from '../Node.ts';
import { getNumberValidator } from '../Validators.ts';
/**
* Brighten Filter.
* @deprecated Use {@link Konva.Filters.Brightness} instead for CSS-compatible behavior.
* This filter uses additive brightness adjustment (adds a constant value to RGB channels).
* @function
* @memberof Konva.Filters
* @param {Object} imageData
* @example
* node.cache();
* node.filters([Konva.Filters.Brighten]);
* node.brightness(0.8);
*/
export const Brighten: Filter = function (imageData) {
const brightness = this.brightness() * 255,
data = imageData.data,
len = data.length;
for (let i = 0; i < len; i += 4) {
// red
data[i] += brightness;
// green
data[i + 1] += brightness;
// blue
data[i + 2] += brightness;
}
};
Factory.addGetterSetter(
Node,
'brightness',
0,
getNumberValidator(),
Factory.afterSetFilter
);
/**
* get/set filter brightness. The brightness is a number between -1 and 1. Positive values
* brighten the pixels and negative values darken them. Use with {@link Konva.Filters.Brighten} filter.
* @name Konva.Node#brightness
* @method
* @param {Number} brightness value between -1 and 1
* @returns {Number}
*/
================================================
FILE: src/filters/Brightness.ts
================================================
import type { Filter } from '../Node.ts';
/**
* Brightness Filter.
* CSS-compatible brightness filter that uses multiplicative approach.
* @function
* @memberof Konva.Filters
* @param {Object} imageData
* @example
* node.cache();
* node.filters([Konva.Filters.Brightness]);
* node.brightness(1.5); // 50% brighter (CSS-compatible)
*/
export const Brightness: Filter = function (imageData) {
const brightness = this.brightness(),
data = imageData.data,
len = data.length;
for (let i = 0; i < len; i += 4) {
// red
data[i] = Math.min(255, data[i] * brightness);
// green
data[i + 1] = Math.min(255, data[i + 1] * brightness);
// blue
data[i + 2] = Math.min(255, data[i + 2] * brightness);
}
};
// Note: brightness property is already defined in Brighten.ts
// This filter reuses the existing brightness property but with CSS-compatible behavior
================================================
FILE: src/filters/Contrast.ts
================================================
import { Factory } from '../Factory.ts';
import type { Filter } from '../Node.ts';
import { Node } from '../Node.ts';
import { getNumberValidator } from '../Validators.ts';
/**
* Contrast Filter.
* @function
* @memberof Konva.Filters
* @param {Object} imageData
* @example
* node.cache();
* node.filters([Konva.Filters.Contrast]);
* node.contrast(10);
*/
export const Contrast: Filter = function (imageData) {
const adjust = Math.pow((this.contrast() + 100) / 100, 2);
const data = imageData.data,
nPixels = data.length;
let red = 150,
green = 150,
blue = 150;
for (let i = 0; i < nPixels; i += 4) {
red = data[i];
green = data[i + 1];
blue = data[i + 2];
//Red channel
red /= 255;
red -= 0.5;
red *= adjust;
red += 0.5;
red *= 255;
//Green channel
green /= 255;
green -= 0.5;
green *= adjust;
green += 0.5;
green *= 255;
//Blue channel
blue /= 255;
blue -= 0.5;
blue *= adjust;
blue += 0.5;
blue *= 255;
red = red < 0 ? 0 : red > 255 ? 255 : red;
green = green < 0 ? 0 : green > 255 ? 255 : green;
blue = blue < 0 ? 0 : blue > 255 ? 255 : blue;
data[i] = red;
data[i + 1] = green;
data[i + 2] = blue;
}
};
/**
* get/set filter contrast. The contrast is a number between -100 and 100.
* Use with {@link Konva.Filters.Contrast} filter.
* @name Konva.Node#contrast
* @method
* @param {Number} contrast value between -100 and 100
* @returns {Number}
*/
Factory.addGetterSetter(
Node,
'contrast',
0,
getNumberValidator(),
Factory.afterSetFilter
);
================================================
FILE: src/filters/Emboss.ts
================================================
import { Factory } from '../Factory.ts';
import type { Filter } from '../Node.ts';
import { Node } from '../Node.ts';
import { getNumberValidator } from '../Validators.ts';
/**
* Emboss Filter.
* @function
* @memberof Konva.Filters
* @param {Object} imageData
* @example
* node.cache();
* node.filters([Konva.Filters.Emboss]);
* node.embossStrength(0.8);
* node.embossWhiteLevel(0.3);
* node.embossDirection('right');
* node.embossBlend(true);
*/
export const Emboss: Filter = function (imageData) {
const data = imageData.data;
const w = imageData.width;
const h = imageData.height;
// Inputs from Konva node
const strength01 = Math.min(1, Math.max(0, this.embossStrength?.() ?? 0.5)); // [0..1]
const whiteLevel01 = Math.min(
1,
Math.max(0, this.embossWhiteLevel?.() ?? 0.5)
); // [0..1]
// Convert string direction to degrees
const directionMap = {
'top-left': 315,
top: 270,
'top-right': 225,
right: 180,
'bottom-right': 135,
bottom: 90,
'bottom-left': 45,
left: 0,
};
const directionDeg =
directionMap[this.embossDirection?.() ?? 'top-left'] ?? 315; // degrees
const blend = !!(this.embossBlend?.() ?? false);
// Internal mapping:
// - "strength" was 0..10; we honor your 0..1 API and scale accordingly.
// - Sobel directional response is roughly in [-1020..1020] for 8-bit luminance; scale to ~±128.
const strength = strength01 * 10;
const bias = whiteLevel01 * 255;
const dirRad = (directionDeg * Math.PI) / 180;
const cx = Math.cos(dirRad);
const cy = Math.sin(dirRad);
const SCALE = (128 / 1020) * strength; // ≈0.1255 * strength
// Precompute luminance (Rec.709)
const src = new Uint8ClampedArray(data); // snapshot
const lum = new Float32Array(w * h);
for (let p = 0, i = 0; i < data.length; i += 4, p++) {
lum[p] = 0.2126 * src[i] + 0.7152 * src[i + 1] + 0.0722 * src[i + 2];
}
// Sobel kernels (flattened)
const Gx = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
const Gy = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
// neighbor offsets around center pixel in lum space
const OFF = [-w - 1, -w, -w + 1, -1, 0, 1, w - 1, w, w + 1];
// Helpers
const clamp8 = (v) => (v < 0 ? 0 : v > 255 ? 255 : v);
// Process: leave a 1px border unchanged (faster/cleaner)
for (let y = 1; y < h - 1; y++) {
for (let x = 1; x < w - 1; x++) {
const p = y * w + x;
// Directional derivative = (cosθ * Gx + sinθ * Gy) • neighborhood(lum)
let sx = 0,
sy = 0;
// unroll loop for speed
sx += lum[p + OFF[0]] * Gx[0];
sy += lum[p + OFF[0]] * Gy[0];
sx += lum[p + OFF[1]] * Gx[1];
sy += lum[p + OFF[1]] * Gy[1];
sx += lum[p + OFF[2]] * Gx[2];
sy += lum[p + OFF[2]] * Gy[2];
sx += lum[p + OFF[3]] * Gx[3];
sy += lum[p + OFF[3]] * Gy[3];
// center has 0 weights in both Sobel masks; can skip if desired
sx += lum[p + OFF[5]] * Gx[5];
sy += lum[p + OFF[5]] * Gy[5];
sx += lum[p + OFF[6]] * Gx[6];
sy += lum[p + OFF[6]] * Gy[6];
sx += lum[p + OFF[7]] * Gx[7];
sy += lum[p + OFF[7]] * Gy[7];
sx += lum[p + OFF[8]] * Gx[8];
sy += lum[p + OFF[8]] * Gy[8];
const r = cx * sx + cy * sy; // directional response
const outGray = clamp8(bias + r * SCALE); // biased, scaled, clamped
const o = p * 4;
if (blend) {
// Add the emboss "relief" around chosen bias to original RGB
const delta = outGray - bias; // symmetric around whiteLevel
data[o] = clamp8(src[o] + delta);
data[o + 1] = clamp8(src[o + 1] + delta);
data[o + 2] = clamp8(src[o + 2] + delta);
data[o + 3] = src[o + 3];
} else {
// Grayscale embossed output
data[o] = data[o + 1] = data[o + 2] = outGray;
data[o + 3] = src[o + 3];
}
}
}
// Copy border (untouched) to keep edges clean
// top & bottom rows
for (let x = 0; x < w; x++) {
let oTop = x * 4,
oBot = ((h - 1) * w + x) * 4;
data[oTop] = src[oTop];
data[oTop + 1] = src[oTop + 1];
data[oTop + 2] = src[oTop + 2];
data[oTop + 3] = src[oTop + 3];
data[oBot] = src[oBot];
data[oBot + 1] = src[oBot + 1];
data[oBot + 2] = src[oBot + 2];
data[oBot + 3] = src[oBot + 3];
}
// left & right columns
for (let y = 1; y < h - 1; y++) {
let oL = y * w * 4,
oR = (y * w + (w - 1)) * 4;
data[oL] = src[oL];
data[oL + 1] = src[oL + 1];
data[oL + 2] = src[oL + 2];
data[oL + 3] = src[oL + 3];
data[oR] = src[oR];
data[oR + 1] = src[oR + 1];
data[oR + 2] = src[oR + 2];
data[oR + 3] = src[oR + 3];
}
return imageData;
};
Factory.addGetterSetter(
Node,
'embossStrength',
0.5,
getNumberValidator(),
Factory.afterSetFilter
);
/**
* get/set emboss strength. Use with {@link Konva.Filters.Emboss} filter.
* @name Konva.Node#embossStrength
* @method
* @param {Number} level between 0 and 1. Default is 0.5
* @returns {Number}
*/
Factory.addGetterSetter(
Node,
'embossWhiteLevel',
0.5,
getNumberValidator(),
Factory.afterSetFilter
);
/**
* get/set emboss white level. Use with {@link Konva.Filters.Emboss} filter.
* @name Konva.Node#embossWhiteLevel
* @method
* @param {Number} embossWhiteLevel between 0 and 1. Default is 0.5
* @returns {Number}
*/
Factory.addGetterSetter(
Node,
'embossDirection',
'top-left',
undefined,
Factory.afterSetFilter
);
/**
* get/set emboss direction. Use with {@link Konva.Filters.Emboss} filter.
* @name Konva.Node#embossDirection
* @method
* @param {String} embossDirection can be top-left, top, top-right, right, bottom-right, bottom, bottom-left or left
* The default is top-left
* @returns {String}
*/
Factory.addGetterSetter(
Node,
'embossBlend',
false,
undefined,
Factory.afterSetFilter
);
/**
* get/set emboss blend. Use with {@link Konva.Filters.Emboss} filter.
* @name Konva.Node#embossBlend
* @method
* @param {Boolean} embossBlend
* @returns {Boolean}
*/
================================================
FILE: src/filters/Enhance.ts
================================================
import { Factory } from '../Factory.ts';
import type { Filter } from '../Node.ts';
import { Node } from '../Node.ts';
import { getNumberValidator } from '../Validators.ts';
function remap(
fromValue: number,
fromMin: number,
fromMax: number,
toMin: number,
toMax: number
) {
// Compute the range of the data
const fromRange = fromMax - fromMin,
toRange = toMax - toMin;
// If either range is 0, then the value can only be mapped to 1 value
if (fromRange === 0) {
return toMin + toRange / 2;
}
if (toRange === 0) {
return toMin;
}
// (1) untranslate, (2) unscale, (3) rescale, (4) retranslate
let toValue = (fromValue - fromMin) / fromRange;
toValue = toRange * toValue + toMin;
return toValue;
}
/**
* Enhance Filter. Adjusts the colors so that they span the widest
* possible range (ie 0-255). Performs w*h pixel reads and w*h pixel
* writes.
* @function
* @name Enhance
* @memberof Konva.Filters
* @param {Object} imageData
* @author ippo615
* @example
* node.cache();
* node.filters([Konva.Filters.Enhance]);
* node.enhance(0.4);
*/
export const Enhance: Filter = function (imageData) {
const data = imageData.data,
nSubPixels = data.length;
let rMin = data[0],
rMax = rMin,
r,
gMin = data[1],
gMax = gMin,
g,
bMin = data[2],
bMax = bMin,
b;
// If we are not enhancing anything - don't do any computation
const enhanceAmount = this.enhance();
if (enhanceAmount === 0) {
return;
}
// 1st Pass - find the min and max for each channel:
for (let i = 0; i < nSubPixels; i += 4) {
r = data[i + 0];
if (r < rMin) {
rMin = r;
} else if (r > rMax) {
rMax = r;
}
g = data[i + 1];
if (g < gMin) {
gMin = g;
} else if (g > gMax) {
gMax = g;
}
b = data[i + 2];
if (b < bMin) {
bMin = b;
} else if (b > bMax) {
bMax = b;
}
//a = data[i + 3];
//if (a < aMin) { aMin = a; } else
//if (a > aMax) { aMax = a; }
}
// If there is only 1 level - don't remap
if (rMax === rMin) {
rMax = 255;
rMin = 0;
}
if (gMax === gMin) {
gMax = 255;
gMin = 0;
}
if (bMax === bMin) {
bMax = 255;
bMin = 0;
}
let rGoalMax: number,
rGoalMin: number,
gGoalMax: number,
gGoalMin: number,
bGoalMax: number,
bGoalMin: number;
// If the enhancement is positive - stretch the histogram
if (enhanceAmount > 0) {
rGoalMax = rMax + enhanceAmount * (255 - rMax);
rGoalMin = rMin - enhanceAmount * (rMin - 0);
gGoalMax = gMax + enhanceAmount * (255 - gMax);
gGoalMin = gMin - enhanceAmount * (gMin - 0);
bGoalMax = bMax + enhanceAmount * (255 - bMax);
bGoalMin = bMin - enhanceAmount * (bMin - 0);
// If the enhancement is negative - compress the histogram
} else {
const rMid = (rMax + rMin) * 0.5;
rGoalMax = rMax + enhanceAmount * (rMax - rMid);
rGoalMin = rMin + enhanceAmount * (rMin - rMid);
const gMid = (gMax + gMin) * 0.5;
gGoalMax = gMax + enhanceAmount * (gMax - gMid);
gGoalMin = gMin + enhanceAmount * (gMin - gMid);
const bMid = (bMax + bMin) * 0.5;
bGoalMax = bMax + enhanceAmount * (bMax - bMid);
bGoalMin = bMin + enhanceAmount * (bMin - bMid);
}
// Pass 2 - remap everything, except the alpha
for (let i = 0; i < nSubPixels; i += 4) {
data[i + 0] = remap(data[i + 0], rMin, rMax, rGoalMin, rGoalMax);
data[i + 1] = remap(data[i + 1], gMin, gMax, gGoalMin, gGoalMax);
data[i + 2] = remap(data[i + 2], bMin, bMax, bGoalMin, bGoalMax);
//data[i + 3] = remap(data[i + 3], aMin, aMax, aGoalMin, aGoalMax);
}
};
/**
* get/set enhance. Use with {@link Konva.Filters.Enhance} filter. -1 to 1 values
* @name Konva.Node#enhance
* @method
* @param {Float} amount
* @returns {Float}
*/
Factory.addGetterSetter(
Node,
'enhance',
0,
getNumberValidator(),
Factory.afterSetFilter
);
================================================
FILE: src/filters/Grayscale.ts
================================================
import type { Filter } from '../Node.ts';
/**
* Grayscale Filter
* @function
* @memberof Konva.Filters
* @param {Object} imageData
* @example
* node.cache();
* node.filters([Konva.Filters.Grayscale]);
*/
export const Grayscale: Filter = function (imageData) {
const data = imageData.data,
len = data.length;
for (let i = 0; i < len; i += 4) {
const brightness = 0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2];
// red
data[i] = brightness;
// green
data[i + 1] = brightness;
// blue
data[i + 2] = brightness;
}
};
================================================
FILE: src/filters/HSL.ts
================================================
import { Factory } from '../Factory.ts';
import type { Filter } from '../Node.ts';
import { Node } from '../Node.ts';
import { getNumberValidator } from '../Validators.ts';
Factory.addGetterSetter(
Node,
'hue',
0,
getNumberValidator(),
Factory.afterSetFilter
);
/**
* get/set hsv hue in degrees. Use with {@link Konva.Filters.HSV} or {@link Konva.Filters.HSL} filter.
* @name Konva.Node#hue
* @method
* @param {Number} hue value between 0 and 359
* @returns {Number}
*/
Factory.addGetterSetter(
Node,
'saturation',
0,
getNumberValidator(),
Factory.afterSetFilter
);
/**
* get/set hsv saturation. Use with {@link Konva.Filters.HSV} or {@link Konva.Filters.HSL} filter.
* @name Konva.Node#saturation
* @method
* @param {Number} saturation 0 is no change, -1.0 halves the saturation, 1.0 doubles, etc..
* @returns {Number}
*/
Factory.addGetterSetter(
Node,
'luminance',
0,
getNumberValidator(),
Factory.afterSetFilter
);
/**
* get/set hsl luminance. Use with {@link Konva.Filters.HSL} filter.
* @name Konva.Node#luminance
* @method
* @param {Number} value from -1 to 1
* @returns {Number}
*/
/**
* HSL Filter. Adjusts the hue, saturation and luminance (or lightness)
* @function
* @memberof Konva.Filters
* @param {Object} imageData
* @author ippo615
* @example
* image.filters([Konva.Filters.HSL]);
* image.luminance(0.2);
*/
export const HSL: Filter = function (imageData) {
const data = imageData.data,
nPixels = data.length,
v = 1,
s = Math.pow(2, this.saturation()),
h = Math.abs(this.hue() + 360) % 360,
l = this.luminance() * 127;
// Basis for the technique used:
// http://beesbuzz.biz/code/hsv_color_transforms.php
// V is the value multiplier (1 for none, 2 for double, 0.5 for half)
// S is the saturation multiplier (1 for none, 2 for double, 0.5 for half)
// H is the hue shift in degrees (0 to 360)
// vsu = V*S*cos(H*PI/180);
// vsw = V*S*sin(H*PI/180);
//[ .299V+.701vsu+.168vsw .587V-.587vsu+.330vsw .114V-.114vsu-.497vsw ] [R]
//[ .299V-.299vsu-.328vsw .587V+.413vsu+.035vsw .114V-.114vsu+.292vsw ]*[G]
//[ .299V-.300vsu+1.25vsw .587V-.588vsu-1.05vsw .114V+.886vsu-.203vsw ] [B]
// Precompute the values in the matrix:
const vsu = v * s * Math.cos((h * Math.PI) / 180),
vsw = v * s * Math.sin((h * Math.PI) / 180);
// (result spot)(source spot)
const rr = 0.299 * v + 0.701 * vsu + 0.167 * vsw,
rg = 0.587 * v - 0.587 * vsu + 0.33 * vsw,
rb = 0.114 * v - 0.114 * vsu - 0.497 * vsw;
const gr = 0.299 * v - 0.299 * vsu - 0.328 * vsw,
gg = 0.587 * v + 0.413 * vsu + 0.035 * vsw,
gb = 0.114 * v - 0.114 * vsu + 0.293 * vsw;
const br = 0.299 * v - 0.3 * vsu + 1.25 * vsw,
bg = 0.587 * v - 0.586 * vsu - 1.05 * vsw,
bb = 0.114 * v + 0.886 * vsu - 0.2 * vsw;
let r: number, g: number, b: number, a: number;
for (let i = 0; i < nPixels; i += 4) {
r = data[i + 0];
g = data[i + 1];
b = data[i + 2];
a = data[i + 3];
data[i + 0] = rr * r + rg * g + rb * b + l;
data[i + 1] = gr * r + gg * g + gb * b + l;
data[i + 2] = br * r + bg * g + bb * b + l;
data[i + 3] = a; // alpha
}
};
================================================
FILE: src/filters/HSV.ts
================================================
import { Factory } from '../Factory.ts';
import type { Filter } from '../Node.ts';
import { Node } from '../Node.ts';
import { getNumberValidator } from '../Validators.ts';
/**
* HSV Filter. Adjusts the hue, saturation and value
* @function
* @name HSV
* @memberof Konva.Filters
* @param {Object} imageData
* @author ippo615
* @example
* image.filters([Konva.Filters.HSV]);
* image.value(200);
*/
export const HSV: Filter = function (imageData) {
const data = imageData.data,
nPixels = data.length,
v = Math.pow(2, this.value()),
s = Math.pow(2, this.saturation()),
h = Math.abs(this.hue() + 360) % 360;
// Basis for the technique used:
// http://beesbuzz.biz/code/hsv_color_transforms.php
// V is the value multiplier (1 for none, 2 for double, 0.5 for half)
// S is the saturation multiplier (1 for none, 2 for double, 0.5 for half)
// H is the hue shift in degrees (0 to 360)
// vsu = V*S*cos(H*PI/180);
// vsw = V*S*sin(H*PI/180);
//[ .299V+.701vsu+.168vsw .587V-.587vsu+.330vsw .114V-.114vsu-.497vsw ] [R]
//[ .299V-.299vsu-.328vsw .587V+.413vsu+.035vsw .114V-.114vsu+.292vsw ]*[G]
//[ .299V-.300vsu+1.25vsw .587V-.588vsu-1.05vsw .114V+.886vsu-.203vsw ] [B]
// Precompute the values in the matrix:
const vsu = v * s * Math.cos((h * Math.PI) / 180),
vsw = v * s * Math.sin((h * Math.PI) / 180);
// (result spot)(source spot)
const rr = 0.299 * v + 0.701 * vsu + 0.167 * vsw,
rg = 0.587 * v - 0.587 * vsu + 0.33 * vsw,
rb = 0.114 * v - 0.114 * vsu - 0.497 * vsw;
const gr = 0.299 * v - 0.299 * vsu - 0.328 * vsw,
gg = 0.587 * v + 0.413 * vsu + 0.035 * vsw,
gb = 0.114 * v - 0.114 * vsu + 0.293 * vsw;
const br = 0.299 * v - 0.3 * vsu + 1.25 * vsw,
bg = 0.587 * v - 0.586 * vsu - 1.05 * vsw,
bb = 0.114 * v + 0.886 * vsu - 0.2 * vsw;
for (let i = 0; i < nPixels; i += 4) {
const r = data[i + 0];
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3];
data[i + 0] = rr * r + rg * g + rb * b;
data[i + 1] = gr * r + gg * g + gb * b;
data[i + 2] = br * r + bg * g + bb * b;
data[i + 3] = a; // alpha
}
};
Factory.addGetterSetter(
Node,
'hue',
0,
getNumberValidator(),
Factory.afterSetFilter
);
/**
* get/set hsv hue in degrees. Use with {@link Konva.Filters.HSV} or {@link Konva.Filters.HSL} filter.
* @name Konva.Node#hue
* @method
* @param {Number} hue value between 0 and 359
* @returns {Number}
*/
Factory.addGetterSetter(
Node,
'saturation',
0,
getNumberValidator(),
Factory.afterSetFilter
);
/**
* get/set hsv saturation. Use with {@link Konva.Filters.HSV} or {@link Konva.Filters.HSL} filter.
* @name Konva.Node#saturation
* @method
* @param {Number} saturation 0 is no change, -1.0 halves the saturation, 1.0 doubles, etc..
* @returns {Number}
*/
Factory.addGetterSetter(
Node,
'value',
0,
getNumberValidator(),
Factory.afterSetFilter
);
/**
* get/set hsv value. Use with {@link Konva.Filters.HSV} filter.
* @name Konva.Node#value
* @method
* @param {Number} value 0 is no change, -1.0 halves the value, 1.0 doubles, etc..
* @returns {Number}
*/
================================================
FILE: src/filters/Invert.ts
================================================
import type { Filter } from '../Node.ts';
/**
* Invert Filter
* @function
* @memberof Konva.Filters
* @param {Object} imageData
* @example
* node.cache();
* node.filters([Konva.Filters.Invert]);
*/
export const Invert: Filter = function (imageData) {
const data = imageData.data,
len = data.length;
for (let i = 0; i < len; i += 4) {
// red
data[i] = 255 - data[i];
// green
data[i + 1] = 255 - data[i + 1];
// blue
data[i + 2] = 255 - data[i + 2];
}
};
================================================
FILE: src/filters/Kaleidoscope.ts
================================================
import { Factory } from '../Factory.ts';
import type { Filter } from '../Node.ts';
import { Node } from '../Node.ts';
import { Util } from '../Util.ts';
import { getNumberValidator } from '../Validators.ts';
/*
* ToPolar Filter. Converts image data to polar coordinates. Performs
* w*h*4 pixel reads and w*h pixel writes. The r axis is placed along
* what would be the y axis and the theta axis along the x axis.
* @function
* @author ippo615
* @memberof Konva.Filters
* @param {ImageData} src, the source image data (what will be transformed)
* @param {ImageData} dst, the destination image data (where it will be saved)
* @param {Object} opt
* @param {Number} [opt.polarCenterX] horizontal location for the center of the circle,
* default is in the middle
* @param {Number} [opt.polarCenterY] vertical location for the center of the circle,
* default is in the middle
*/
const ToPolar = function (src, dst, opt) {
const srcPixels = src.data,
dstPixels = dst.data,
xSize = src.width,
ySize = src.height,
xMid = opt.polarCenterX || xSize / 2,
yMid = opt.polarCenterY || ySize / 2;
// Find the largest radius
let rMax = Math.sqrt(xMid * xMid + yMid * yMid);
let x = xSize - xMid;
let y = ySize - yMid;
const rad = Math.sqrt(x * x + y * y);
rMax = rad > rMax ? rad : rMax;
// We'll be uisng y as the radius, and x as the angle (theta=t)
const rSize = ySize,
tSize = xSize;
// We want to cover all angles (0-360) and we need to convert to
// radians (*PI/180)
const conversion = ((360 / tSize) * Math.PI) / 180;
// var x1, x2, x1i, x2i, y1, y2, y1i, y2i, scale;
for (let theta = 0; theta < tSize; theta += 1) {
const sin = Math.sin(theta * conversion);
const cos = Math.cos(theta * conversion);
for (let radius = 0; radius < rSize; radius += 1) {
x = Math.floor(xMid + ((rMax * radius) / rSize) * cos);
y = Math.floor(yMid + ((rMax * radius) / rSize) * sin);
let i = (y * xSize + x) * 4;
const r = srcPixels[i + 0];
const g = srcPixels[i + 1];
const b = srcPixels[i + 2];
const a = srcPixels[i + 3];
// Store it
//i = (theta * xSize + radius) * 4;
i = (theta + radius * xSize) * 4;
dstPixels[i + 0] = r;
dstPixels[i + 1] = g;
dstPixels[i + 2] = b;
dstPixels[i + 3] = a;
}
}
};
/*
* FromPolar Filter. Converts image data from polar coordinates back to rectangular.
* Performs w*h*4 pixel reads and w*h pixel writes.
* @function
* @author ippo615
* @memberof Konva.Filters
* @param {ImageData} src, the source image data (what will be transformed)
* @param {ImageData} dst, the destination image data (where it will be saved)
* @param {Object} opt
* @param {Number} [opt.polarCenterX] horizontal location for the center of the circle,
* default is in the middle
* @param {Number} [opt.polarCenterY] vertical location for the center of the circle,
* default is in the middle
* @param {Number} [opt.polarRotation] amount to rotate the image counterclockwis,
* 0 is no rotation, 360 degrees is a full rotation
*/
const FromPolar = function (src, dst, opt) {
const srcPixels = src.data,
dstPixels = dst.data,
xSize = src.width,
ySize = src.height,
xMid = opt.polarCenterX || xSize / 2,
yMid = opt.polarCenterY || ySize / 2;
// Find the largest radius
let rMax = Math.sqrt(xMid * xMid + yMid * yMid);
let x = xSize - xMid;
let y = ySize - yMid;
const rad = Math.sqrt(x * x + y * y);
rMax = rad > rMax ? rad : rMax;
// We'll be uisng x as the radius, and y as the angle (theta=t)
const rSize = ySize,
tSize = xSize,
phaseShift = opt.polarRotation || 0;
// We need to convert to degrees and we need to make sure
// it's between (0-360)
// var conversion = tSize/360*180/Math.PI;
//var conversion = tSize/360*180/Math.PI;
let x1, y1;
for (x = 0; x < xSize; x += 1) {
for (y = 0; y < ySize; y += 1) {
const dx = x - xMid;
const dy = y - yMid;
const radius = (Math.sqrt(dx * dx + dy * dy) * rSize) / rMax;
let theta =
((Math.atan2(dy, dx) * 180) / Math.PI + 360 + phaseShift) % 360;
theta = (theta * tSize) / 360;
x1 = Math.floor(theta);
y1 = Math.floor(radius);
let i = (y1 * xSize + x1) * 4;
const r = srcPixels[i + 0];
const g = srcPixels[i + 1];
const b = srcPixels[i + 2];
const a = srcPixels[i + 3];
// Store it
i = (y * xSize + x) * 4;
dstPixels[i + 0] = r;
dstPixels[i + 1] = g;
dstPixels[i + 2] = b;
dstPixels[i + 3] = a;
}
}
};
//Konva.Filters.ToPolar = Util._FilterWrapDoubleBuffer(ToPolar);
//Konva.Filters.FromPolar = Util._FilterWrapDoubleBuffer(FromPolar);
// create a temporary canvas for working - shared between multiple calls
/*
* Kaleidoscope Filter.
* @function
* @name Kaleidoscope
* @author ippo615
* @memberof Konva.Filters
* @example
* node.cache();
* node.filters([Konva.Filters.Kaleidoscope]);
* node.kaleidoscopePower(3);
* node.kaleidoscopeAngle(45);
*/
export const Kaleidoscope: Filter = function (imageData) {
const xSize = imageData.width,
ySize = imageData.height;
let x, y, xoff, i, r, g, b, a, srcPos, dstPos;
let power = Math.round(this.kaleidoscopePower());
const angle = Math.round(this.kaleidoscopeAngle());
const offset = Math.floor((xSize * (angle % 360)) / 360);
if (power < 1) {
return;
}
// Work with our shared buffer canvas
const tempCanvas = Util.createCanvasElement();
tempCanvas.width = xSize;
tempCanvas.height = ySize;
const scratchData = tempCanvas
.getContext('2d')!
.getImageData(0, 0, xSize, ySize);
Util.releaseCanvas(tempCanvas);
// Convert thhe original to polar coordinates
ToPolar(imageData, scratchData, {
polarCenterX: xSize / 2,
polarCenterY: ySize / 2,
});
// Determine how big each section will be, if it's too small
// make it bigger
let minSectionSize = xSize / Math.pow(2, power);
while (minSectionSize <= 8) {
minSectionSize = minSectionSize * 2;
power -= 1;
}
minSectionSize = Math.ceil(minSectionSize);
let sectionSize = minSectionSize;
// Copy the offset region to 0
// Depending on the size of filter and location of the offset we may need
// to copy the section backwards to prevent it from rewriting itself
let xStart = 0,
xEnd = sectionSize,
xDelta = 1;
if (offset + minSectionSize > xSize) {
xStart = sectionSize;
xEnd = 0;
xDelta = -1;
}
for (y = 0; y < ySize; y += 1) {
for (x = xStart; x !== xEnd; x += xDelta) {
xoff = Math.round(x + offset) % xSize;
srcPos = (xSize * y + xoff) * 4;
r = scratchData.data[srcPos + 0];
g = scratchData.data[srcPos + 1];
b = scratchData.data[srcPos + 2];
a = scratchData.data[srcPos + 3];
dstPos = (xSize * y + x) * 4;
scratchData.data[dstPos + 0] = r;
scratchData.data[dstPos + 1] = g;
scratchData.data[dstPos + 2] = b;
scratchData.data[dstPos + 3] = a;
}
}
// Perform the actual effect
for (y = 0; y < ySize; y += 1) {
sectionSize = Math.floor(minSectionSize);
for (i = 0; i < power; i += 1) {
for (x = 0; x < sectionSize + 1; x += 1) {
srcPos = (xSize * y + x) * 4;
r = scratchData.data[srcPos + 0];
g = scratchData.data[srcPos + 1];
b = scratchData.data[srcPos + 2];
a = scratchData.data[srcPos + 3];
dstPos = (xSize * y + sectionSize * 2 - x - 1) * 4;
scratchData.data[dstPos + 0] = r;
scratchData.data[dstPos + 1] = g;
scratchData.data[dstPos + 2] = b;
scratchData.data[dstPos + 3] = a;
}
sectionSize *= 2;
}
}
// Convert back from polar coordinates
FromPolar(scratchData, imageData, { polarRotation: 0 });
};
/**
* get/set kaleidoscope power. Use with {@link Konva.Filters.Kaleidoscope} filter.
* @name Konva.Node#kaleidoscopePower
* @method
* @param {Integer} power of kaleidoscope
* @returns {Integer}
*/
Factory.addGetterSetter(
Node,
'kaleidoscopePower',
2,
getNumberValidator(),
Factory.afterSetFilter
);
/**
* get/set kaleidoscope angle. Use with {@link Konva.Filters.Kaleidoscope} filter.
* @name Konva.Node#kaleidoscopeAngle
* @method
* @param {Integer} degrees
* @returns {Integer}
*/
Factory.addGetterSetter(
Node,
'kaleidoscopeAngle',
0,
getNumberValidator(),
Factory.afterSetFilter
);
================================================
FILE: src/filters/Mask.ts
================================================
import { Factory } from '../Factory.ts';
import type { Filter } from '../Node.ts';
import { Node } from '../Node.ts';
import { getNumberValidator } from '../Validators.ts';
function pixelAt(idata, x: number, y: number) {
let idx = (y * idata.width + x) * 4;
const d: Array = [];
d.push(
idata.data[idx++],
idata.data[idx++],
idata.data[idx++],
idata.data[idx++]
);
return d;
}
function rgbDistance(p1, p2) {
return Math.sqrt(
Math.pow(p1[0] - p2[0], 2) +
Math.pow(p1[1] - p2[1], 2) +
Math.pow(p1[2] - p2[2], 2)
);
}
function rgbMean(pTab) {
const m = [0, 0, 0];
for (let i = 0; i < pTab.length; i++) {
m[0] += pTab[i][0];
m[1] += pTab[i][1];
m[2] += pTab[i][2];
}
m[0] /= pTab.length;
m[1] /= pTab.length;
m[2] /= pTab.length;
return m;
}
function backgroundMask(idata, threshold) {
const rgbv_no = pixelAt(idata, 0, 0);
const rgbv_ne = pixelAt(idata, idata.width - 1, 0);
const rgbv_so = pixelAt(idata, 0, idata.height - 1);
const rgbv_se = pixelAt(idata, idata.width - 1, idata.height - 1);
const thres = threshold || 10;
if (
rgbDistance(rgbv_no, rgbv_ne) < thres &&
rgbDistance(rgbv_ne, rgbv_se) < thres &&
rgbDistance(rgbv_se, rgbv_so) < thres &&
rgbDistance(rgbv_so, rgbv_no) < thres
) {
// Mean color
const mean = rgbMean([rgbv_ne, rgbv_no, rgbv_se, rgbv_so]);
// Mask based on color distance
const mask: Array = [];
for (let i = 0; i < idata.width * idata.height; i++) {
const d = rgbDistance(mean, [
idata.data[i * 4],
idata.data[i * 4 + 1],
idata.data[i * 4 + 2],
]);
mask[i] = d < thres ? 0 : 255;
}
return mask;
}
}
function applyMask(idata, mask) {
for (let i = 0; i < idata.width * idata.height; i++) {
idata.data[4 * i + 3] = mask[i];
}
}
function erodeMask(mask, sw, sh) {
const weights = [1, 1, 1, 1, 0, 1, 1, 1, 1];
const side = Math.round(Math.sqrt(weights.length));
const halfSide = Math.floor(side / 2);
const maskResult: Array = [];
for (let y = 0; y < sh; y++) {
for (let x = 0; x < sw; x++) {
const so = y * sw + x;
let a = 0;
for (let cy = 0; cy < side; cy++) {
for (let cx = 0; cx < side; cx++) {
const scy = y + cy - halfSide;
const scx = x + cx - halfSide;
if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) {
const srcOff = scy * sw + scx;
const wt = weights[cy * side + cx];
a += mask[srcOff] * wt;
}
}
}
maskResult[so] = a === 255 * 8 ? 255 : 0;
}
}
return maskResult;
}
function dilateMask(mask, sw, sh) {
const weights = [1, 1, 1, 1, 1, 1, 1, 1, 1];
const side = Math.round(Math.sqrt(weights.length));
const halfSide = Math.floor(side / 2);
const maskResult: Array