canvas-transform-context
Advanced tools
Comparing version 0.1.5 to 1.0.0
@@ -1,46 +0,3 @@ | ||
export interface TransformCanvasRenderingContext2D extends CanvasRenderingContext2D { | ||
getTransform(): SVGMatrix; | ||
transformedPoint(x: number, y: number): DOMPoint; | ||
clearCanvas(): void; | ||
/** | ||
* Marks the start of a canvas drag. | ||
* Should be used on the onmousedown event | ||
* @param e | ||
*/ | ||
beginPan(e: MouseEvent): void; | ||
/** | ||
* Drags the canvas. | ||
* Should be used on the onmousemove event | ||
* @param e | ||
*/ | ||
pan(e: MouseEvent): void; | ||
/** | ||
* Ends the canvas draw. | ||
* Should be used on the onmousemove event | ||
* @param e | ||
*/ | ||
endPan(e: MouseEvent): void; | ||
/** | ||
* Zooms in or out | ||
* @param amount Amount in integers to zoom by. Applies zoom on top of previous zoom | ||
* @param factor Zoom factor. Defaults to 1.1 | ||
* @param center The center to zoom to. If undefined, will zoom to the last mouse pos in endDraw | ||
*/ | ||
zoom(amount: number, factor?: number, center?: { | ||
x: number; | ||
y: number; | ||
}): number; | ||
} | ||
/** | ||
* Type guard for transformed context | ||
* @param ctx | ||
* @returns | ||
*/ | ||
export declare function isTransformedContext(ctx: CanvasRenderingContext2D): ctx is TransformCanvasRenderingContext2D; | ||
/** | ||
* Extends a canvas context IN PLACE. | ||
* The return value is for type change typescript usage | ||
* @param ctx | ||
* @returns The canvas context. | ||
*/ | ||
export declare function toTransformedContext(ctx: CanvasRenderingContext2D): TransformCanvasRenderingContext2D; | ||
import { toTransformedContext, isTransformedContext, TransformCanvasRenderingContext2D } from "./toTransformedContext"; | ||
import { default as TransformContext } from "./TransformContext"; | ||
export { TransformContext, toTransformedContext, isTransformedContext, TransformCanvasRenderingContext2D, }; |
@@ -1,115 +0,3 @@ | ||
/** | ||
* Type guard for transformed context | ||
* @param ctx | ||
* @returns | ||
*/ | ||
export function isTransformedContext(ctx) { | ||
return "zoom" in ctx && "beginPan" in ctx && "pan" in ctx && "endPan" in ctx; | ||
} | ||
/** | ||
* Extends a canvas context IN PLACE. | ||
* The return value is for type change typescript usage | ||
* @param ctx | ||
* @returns The canvas context. | ||
*/ | ||
export function toTransformedContext(ctx) { | ||
if (isTransformedContext(ctx)) { | ||
console.warn("[canvas-transform] Canvas is already a transformed canvas!"); | ||
return ctx; | ||
} | ||
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); | ||
let xform = svg.createSVGMatrix(); | ||
ctx.getTransform = function () { | ||
return xform; | ||
}; | ||
const savedTransforms = []; | ||
ctx.save = function () { | ||
savedTransforms.push(xform.translate(0, 0)); | ||
return CanvasRenderingContext2D.prototype.save.call(ctx); | ||
}; | ||
ctx.restore = function () { | ||
xform = savedTransforms.pop(); | ||
return CanvasRenderingContext2D.prototype.restore.call(ctx); | ||
}; | ||
ctx.scale = function (sx, sy) { | ||
xform = xform.scale(sx, sy); | ||
return CanvasRenderingContext2D.prototype.scale.call(ctx, sx, sy); | ||
}; | ||
ctx.rotate = function (radians) { | ||
xform = xform.rotate((radians * 180) / Math.PI); | ||
return CanvasRenderingContext2D.prototype.rotate.call(ctx, radians); | ||
}; | ||
ctx.translate = function (dx, dy) { | ||
xform = xform.translate(dx, dy); | ||
return CanvasRenderingContext2D.prototype.translate.call(ctx, dx, dy); | ||
}; | ||
ctx.transform = function (a, b, c, d, e, f) { | ||
let m2 = svg.createSVGMatrix(); | ||
m2.a = a; | ||
m2.b = b; | ||
m2.c = c; | ||
m2.d = d; | ||
m2.e = e; | ||
m2.f = f; | ||
xform = xform.multiply(m2); | ||
return CanvasRenderingContext2D.prototype.transform.call(ctx, a, b, c, d, e, f); | ||
}; | ||
ctx.setTransform = function (a, b, c, d, e, f) { | ||
if (typeof a === "number") { | ||
xform.a = a; | ||
xform.b = b; | ||
xform.c = c; | ||
xform.d = d; | ||
xform.e = e; | ||
xform.f = f; | ||
return CanvasRenderingContext2D.prototype.setTransform.call(ctx, a, b, c, d, e, f); | ||
} | ||
else { | ||
return CanvasRenderingContext2D.prototype.setTransform.call(ctx, a); | ||
} | ||
}; | ||
// Extensions | ||
let pt = svg.createSVGPoint(); | ||
ctx.transformedPoint = function (x, y) { | ||
pt.x = x; | ||
pt.y = y; | ||
return pt.matrixTransform(xform.inverse()); | ||
}; | ||
let lastX = 0, lastY = 0, dragged = false, dragStart = undefined; | ||
ctx.clearCanvas = function () { | ||
var p1 = this.transformedPoint(0, 0); | ||
var p2 = this.transformedPoint(this.canvas.width, this.canvas.height); | ||
this.clearRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y); | ||
}; | ||
ctx.beginPan = function (e) { | ||
lastX = e.offsetX || e.pageX - ctx.canvas.offsetLeft; | ||
lastY = e.offsetY || e.pageY - ctx.canvas.offsetTop; | ||
dragStart = this.transformedPoint(lastX, lastY); | ||
dragged = false; | ||
}; | ||
ctx.pan = function (e) { | ||
lastX = e.offsetX || e.pageX - ctx.canvas.offsetLeft; | ||
lastY = e.offsetY || e.pageY - ctx.canvas.offsetTop; | ||
dragged = true; | ||
if (dragStart) { | ||
let pt = this.transformedPoint(lastX, lastY); | ||
this.translate(pt.x - dragStart.x, pt.y - dragStart.y); | ||
} | ||
}; | ||
ctx.endPan = function () { | ||
dragStart = null; | ||
}; | ||
let zoom = 0; | ||
ctx.zoom = function (amount, zoomFactor = 1.1, center) { | ||
let pt = center | ||
? this.transformedPoint(center.x, center.y) | ||
: this.transformedPoint(lastX, lastY); | ||
zoom + amount; | ||
this.translate(pt.x, pt.y); | ||
const factor = Math.pow(zoomFactor, amount); | ||
this.scale(factor, factor); | ||
this.translate(-pt.x, -pt.y); | ||
return zoom; | ||
}; | ||
return ctx; | ||
} | ||
import { toTransformedContext, isTransformedContext, } from "./toTransformedContext"; | ||
import { default as TransformContext } from "./TransformContext"; | ||
export { TransformContext, toTransformedContext, isTransformedContext, }; |
{ | ||
"name": "canvas-transform-context", | ||
"version": "0.1.5", | ||
"version": "1.0.0", | ||
"description": "A wrapper for HTML canvas context for easy zooming/panning/translating", | ||
@@ -8,3 +8,4 @@ "main": "dist/index.js", | ||
"scripts": { | ||
"build": "tsc && uglifyjs --compress --mangle -o ./dist/index.min.js -- ./dist/index.js", | ||
"build": "tsc && rollup dist/index.js --file dist/bundle.mjs --format esm && uglifyjs dist/bundle.mjs --compress -o dist/bundle.min.js", | ||
"build:test": "npm run build && cp dist/bundle.mjs examples/dist/canvas-transform.js", | ||
"deploy": "gh-pages --dist examples", | ||
@@ -28,6 +29,8 @@ "test": "jest --env=jsdom" | ||
"gh-pages": "^4.0.0", | ||
"tsc": "^2.0.4", | ||
"typescript": "^4.7.4", | ||
"uglifyjs": "^2.4.11" | ||
"husky": "^8.0.1", | ||
"minify": "^9.1.0", | ||
"rollup": "^2.76.0", | ||
"terser": "^5.14.1", | ||
"typescript": "^4.7.4" | ||
} | ||
} |
216
README.md
@@ -5,12 +5,12 @@ # Canvas Transform Context | ||
A canvas context extension based on [this](http://phrogz.net/tmp/canvas_zoom_to_cursor.html) example by [phrogz](https://stackoverflow.com/users/405017/phrogz). | ||
A canvas context extension based on [this](http://phrogz.net/tmp/canvas_zoom_to_cursor.html) example by [phrogz](https://stackoverflow.com/users/405017/phrogz). | ||
Extends the 2d canvas context to support zooming and panning, allowing any canvas to be easily extended with panning and zooming functionalities. Perfect for visual web apps that requires extra canvas functionalities without the hassle of custom canvas implementations. | ||
A class wrapper for a 2D canvas context that keeps track of transform information, allowing for easy coordinate control with scaled/transformed canvases. Perfect for visual web apps that requires extra canvas functionalities without the hassle of custom canvas implementations. | ||
## [Demo](https://poohcom1.github.io/canvas-transform-context/basic/) | ||
## Installation | ||
### via npm | ||
``` | ||
@@ -21,7 +21,9 @@ npm i canvas-transform-context | ||
```javascript | ||
import { toTransformedContext } from "canvas-transform-context" | ||
import { TransformContext } from "canvas-transform-context" | ||
``` | ||
### via browser | ||
```javascript | ||
import { toTransformedContext } from "https://unpkg.com/canvas-transform-context@latest/dist/index.min.js"; | ||
import { TransformContext } from "https://unpkg.com/canvas-transform-context@latest/dist/bundle.min.js"; | ||
``` | ||
@@ -31,64 +33,178 @@ | ||
Basic setup | ||
```javascript | ||
import { toTransformedContext } from "https://unpkg.com/canvas-transform-context@latest/dist/index.min.js" | ||
// import { toTransformedContext } from "canvas-transform" | ||
const canvas = getDocumentById("myCanvas") | ||
const canvas = getDocumentById(/* canvas id */) | ||
const ctx = canvas.getContext('2d') | ||
toTransformedContext(ctx) | ||
const transformCtx = new TransformContext(ctx); | ||
// Create reusable draw function | ||
function draw(ctx) { | ||
/* draw on canvas */ | ||
} | ||
transformCtx.onDraw((ctx) => { | ||
/* Draw on canvas... */ | ||
}) | ||
// Mouse dragging | ||
canvas.addEventListener("mousedown", (e) => { | ||
ctx.beginPan(e); | ||
draw(ctx); | ||
}); | ||
canvas.addEventListener("mousemove", (e) => { | ||
ctx.pan(e); | ||
draw(ctx); | ||
}); | ||
canvas.addEventListener("mouseup", (e) => { | ||
ctx.endPan(e); | ||
draw(ctx); | ||
}); | ||
canvas.addEventListener("mousedown", (e) => transformCtx.beginMousePan(e)); | ||
canvas.addEventListener("mousemove", (e) => transformCtx.moveMousePan(e)); | ||
canvas.addEventListener("mouseup", (e) => transformCtx.endPan(e)); | ||
// Wheel zooming | ||
canvas.addEventListener("wheel", (e) => { | ||
ctx.zoom(-Math.sign(e.deltaY)); | ||
draw(ctx); | ||
}) | ||
canvas.addEventListener("wheel", (e) => transformCtx.zoomByMouse(e)); | ||
``` | ||
## Documentation | ||
# Documentation | ||
| Function | Description | | ||
| -- | -- | | ||
| toTransformedContext(ctx) | Extends the canvas context with new methods for rotation and panning (see below). Since the context is directly modified, the value does not need to be reassigned. However, the function does also return the modified context for the sake of typing when using with Typescript. | | ||
## Action Methods | ||
### Utility methods | ||
| Method | Description | | ||
| -- | -- | | ||
| ctx.transformedPoint(x, y)| Converts a coordinate to the correct translated/scaled coordinates. Returns a DOMPoint (contains `x` and `y` properties). | | ||
Batteries-included methods for commonly use actions, included methods that can directly take mouse events as a parameter. | ||
### Panning methods | ||
| Method | Description | | ||
| -- | -- | | ||
| ctx.beginPan(mouseEvent) | Sets the initial panning point. Call from `mousedown`. | | ||
| ctx.pan(mouseEvent) | Pans the canvas. Call from `mousemove`. | | ||
| ctx.endPan(mouseEvent) | Stops the panning. Call form `mouseup`. | | ||
### Zooming methods | ||
| Method | Description | | ||
| -- | -- | | ||
| ctx.zoom(amount, factor?, center?) | Zooms the canvas. `amount` represents the increment to zoom (in integers). `factor` is the percentage to scale by. Defaults to 1.1. `center` is the canvas position to zoom to; if undefined, it will infer from the latest panned position from `endPan`. | | ||
### `beginMousePan(e)` | ||
Begins a pan given the current position from the mouse event | ||
| Param | | ||
| ------- | | ||
| e | | ||
### `moveMousePan(e)` | ||
Pans the canvas to the new position from the mouse event. | ||
Does nothing if beginMousePan wasn't called, or if endPan was just called | ||
| Param | | ||
| ------- | | ||
| e | | ||
### `endMousePan()` | ||
Ends a mouse pan | ||
### `zoomByMouse(e, zoomScale)` | ||
Zooms via the mouse wheel event | ||
| Param | Default | Description | | ||
| ----------- | ------------------ | ------------------------------------------------- | | ||
| e | | mouse wheel event | | ||
| zoomScale | <code>1.1</code> | The scale percentage to zoom by. Default is 1.1 | | ||
### `beginPan(start, transform)` | ||
Sets the anchor for a panning action | ||
| Param | Default | Description | | ||
| ----------- | ------------------- | --------------------------------------------------- | | ||
| start | | Starting coordinates for a pan | | ||
| transform | <code>true</code> | Whether or not to transform the start coordinates | | ||
### `movePan(current, transform)` | ||
Pans the canvas to the new coordinates given the starting point in beginPan. | ||
Does nothing if beginPan was not called, or if endPan was just called | ||
| Param | Default | Description | | ||
| ----------- | ------------------- | --------------------------------------------------- | | ||
| current | | | | ||
| transform | <code>true</code> | Whether or not to transform the start coordinates | | ||
### `endPan()` | ||
Stops a pan | ||
### `zoomBy(amount, zoomScale, center, transform) ⇒` | ||
Zoom by a given integer amount | ||
**Returns**: Current zoom amount in integers | ||
| Param | Default | Description | | ||
| ----------- | ------------------------ | --------------------------------------------------------------------------------------------- | | ||
| amount | | Amount to zoom by in integers. Positive integer zooms in | | ||
| zoomScale | <code>1.1</code> | The scale percentage to zoom by. Default is 1.1 | | ||
| center | <code>undefined</code> | The point to zoom in towards. If undefined, it will zoom towards the latest panned position | | ||
| transform | <code>true</code> | Whether or not to transform the center coordinates | | ||
### `reset()` | ||
Resets all transformations | ||
## Action Helpers | ||
### `onDraw(callback)` | ||
Creates a callback to be called after each action method above. | ||
| Param | Description | | ||
| ------------- | -------------------- | | ||
| callback | A callback function with the canvas context as a parameter | | ||
## Transform Helpers | ||
Helper methods to deal with coordinate transformations | ||
### `transformPoint(canvasPoint) ⇒` | ||
Converts canvas coordinates to transformed coordinates | ||
**Returns**: Transformed coordinates | ||
| Param | Description | | ||
| ------------- | -------------------- | | ||
| canvasPoint | Canvas coordinates | | ||
### `mouseToTransformed(e) ⇒` | ||
Converts a mouse event to the transformed coordinates within the canvas | ||
**Returns**: Transformed point | ||
| Param | Description | | ||
| ------- | ------------- | | ||
| e | mouse event | | ||
### `clearCanvas()` | ||
Clear the canvas given the current transformations | ||
## General Helpers | ||
General purpose canvas helpers unrelated to transform | ||
### `mouseToCanvas(e) ⇒` | ||
Converts a mouse event to the correct canvas coordinates | ||
**Returns**: Canvas coordinates | ||
| Param | Description | | ||
| ------- | ------------- | | ||
| e | mouse event | | ||
## Attributions | ||
Main implementation based on code by phrogz: | ||
- http://phrogz.net/tmp/canvas_zoom_to_cursor.html | ||
- https://github.com/Phrogz | ||
- http://phrogz.net/tmp/canvas_zoom_to_cursor.html | ||
- https://github.com/Phrogz |
224
src/index.ts
@@ -1,213 +0,13 @@ | ||
export interface TransformCanvasRenderingContext2D | ||
extends CanvasRenderingContext2D { | ||
getTransform(): SVGMatrix; | ||
transformedPoint(x: number, y: number): DOMPoint; | ||
clearCanvas(): void; | ||
import { | ||
toTransformedContext, | ||
isTransformedContext, | ||
TransformCanvasRenderingContext2D, | ||
} from "./toTransformedContext"; | ||
import { default as TransformContext } from "./TransformContext"; | ||
// Transform functions | ||
/** | ||
* Marks the start of a canvas drag. | ||
* Should be used on the onmousedown event | ||
* @param e | ||
*/ | ||
beginPan(e: MouseEvent): void; | ||
/** | ||
* Drags the canvas. | ||
* Should be used on the onmousemove event | ||
* @param e | ||
*/ | ||
pan(e: MouseEvent): void; | ||
/** | ||
* Ends the canvas draw. | ||
* Should be used on the onmousemove event | ||
* @param e | ||
*/ | ||
endPan(e: MouseEvent): void; | ||
/** | ||
* Zooms in or out | ||
* @param amount Amount in integers to zoom by. Applies zoom on top of previous zoom | ||
* @param factor Zoom factor. Defaults to 1.1 | ||
* @param center The center to zoom to. If undefined, will zoom to the last mouse pos in endDraw | ||
*/ | ||
zoom( | ||
amount: number, | ||
factor?: number, | ||
center?: { x: number; y: number } | ||
): number; | ||
} | ||
/** | ||
* Type guard for transformed context | ||
* @param ctx | ||
* @returns | ||
*/ | ||
export function isTransformedContext( | ||
ctx: CanvasRenderingContext2D | ||
): ctx is TransformCanvasRenderingContext2D { | ||
return "zoom" in ctx && "beginPan" in ctx && "pan" in ctx && "endPan" in ctx; | ||
} | ||
/** | ||
* Extends a canvas context IN PLACE. | ||
* The return value is for type change typescript usage | ||
* @param ctx | ||
* @returns The canvas context. | ||
*/ | ||
export function toTransformedContext( | ||
ctx: CanvasRenderingContext2D | ||
): TransformCanvasRenderingContext2D { | ||
if (isTransformedContext(ctx)) { | ||
console.warn("[canvas-transform] Canvas is already a transformed canvas!"); | ||
return ctx; | ||
} | ||
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); | ||
let xform = svg.createSVGMatrix(); | ||
ctx.getTransform = function () { | ||
return xform; | ||
}; | ||
const savedTransforms: DOMMatrix[] = []; | ||
ctx.save = function () { | ||
savedTransforms.push(xform.translate(0, 0)); | ||
return CanvasRenderingContext2D.prototype.save.call(ctx); | ||
}; | ||
ctx.restore = function () { | ||
xform = savedTransforms.pop()!; | ||
return CanvasRenderingContext2D.prototype.restore.call(ctx); | ||
}; | ||
ctx.scale = function (sx, sy) { | ||
xform = xform.scale(sx, sy); | ||
return CanvasRenderingContext2D.prototype.scale.call(ctx, sx, sy); | ||
}; | ||
ctx.rotate = function (radians) { | ||
xform = xform.rotate((radians * 180) / Math.PI); | ||
return CanvasRenderingContext2D.prototype.rotate.call(ctx, radians); | ||
}; | ||
ctx.translate = function (dx, dy) { | ||
xform = xform.translate(dx, dy); | ||
return CanvasRenderingContext2D.prototype.translate.call(ctx, dx, dy); | ||
}; | ||
ctx.transform = function (a, b, c, d, e, f) { | ||
let m2 = svg.createSVGMatrix(); | ||
m2.a = a; | ||
m2.b = b; | ||
m2.c = c; | ||
m2.d = d; | ||
m2.e = e; | ||
m2.f = f; | ||
xform = xform.multiply(m2); | ||
return CanvasRenderingContext2D.prototype.transform.call( | ||
ctx, | ||
a, | ||
b, | ||
c, | ||
d, | ||
e, | ||
f | ||
); | ||
}; | ||
ctx.setTransform = function ( | ||
a: number | DOMMatrix2DInit, | ||
b?: number, | ||
c?: number, | ||
d?: number, | ||
e?: number, | ||
f?: number | ||
) { | ||
if (typeof a === "number") { | ||
xform.a = a; | ||
xform.b = b; | ||
xform.c = c; | ||
xform.d = d; | ||
xform.e = e; | ||
xform.f = f; | ||
return CanvasRenderingContext2D.prototype.setTransform.call( | ||
ctx, | ||
a, | ||
b, | ||
c, | ||
d, | ||
e, | ||
f | ||
); | ||
} else { | ||
return CanvasRenderingContext2D.prototype.setTransform.call(ctx, a); | ||
} | ||
}; | ||
// Extensions | ||
let pt = svg.createSVGPoint(); | ||
(ctx as TransformCanvasRenderingContext2D).transformedPoint = function ( | ||
x, | ||
y | ||
) { | ||
pt.x = x; | ||
pt.y = y; | ||
return pt.matrixTransform(xform.inverse()); | ||
}; | ||
let lastX = 0, | ||
lastY = 0, | ||
dragged = false, | ||
dragStart: DOMPoint | undefined = undefined; | ||
(ctx as TransformCanvasRenderingContext2D).clearCanvas = function () { | ||
var p1 = this.transformedPoint(0, 0); | ||
var p2 = this.transformedPoint(this.canvas.width, this.canvas.height); | ||
this.clearRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y); | ||
}; | ||
(ctx as TransformCanvasRenderingContext2D).beginPan = function (e) { | ||
lastX = e.offsetX || e.pageX - ctx.canvas.offsetLeft; | ||
lastY = e.offsetY || e.pageY - ctx.canvas.offsetTop; | ||
dragStart = this.transformedPoint(lastX, lastY); | ||
dragged = false; | ||
}; | ||
(ctx as TransformCanvasRenderingContext2D).pan = function (e) { | ||
lastX = e.offsetX || e.pageX - ctx.canvas.offsetLeft; | ||
lastY = e.offsetY || e.pageY - ctx.canvas.offsetTop; | ||
dragged = true; | ||
if (dragStart) { | ||
let pt = this.transformedPoint(lastX, lastY); | ||
this.translate(pt.x - dragStart.x, pt.y - dragStart.y); | ||
} | ||
}; | ||
(ctx as TransformCanvasRenderingContext2D).endPan = function () { | ||
dragStart = null; | ||
}; | ||
let zoom = 0; | ||
(ctx as TransformCanvasRenderingContext2D).zoom = function ( | ||
amount, | ||
zoomFactor = 1.1, | ||
center?: { x: number; y: number } | ||
) { | ||
let pt = center | ||
? this.transformedPoint(center.x, center.y) | ||
: this.transformedPoint(lastX, lastY); | ||
zoom + amount; | ||
this.translate(pt.x, pt.y); | ||
const factor = Math.pow(zoomFactor, amount); | ||
this.scale(factor, factor); | ||
this.translate(-pt.x, -pt.y); | ||
return zoom; | ||
}; | ||
return ctx as TransformCanvasRenderingContext2D; | ||
} | ||
export { | ||
TransformContext, | ||
toTransformedContext, | ||
isTransformedContext, | ||
TransformCanvasRenderingContext2D, | ||
}; |
{ | ||
"compilerOptions": { | ||
"strict": true, | ||
"lib": ["es6", "dom", "dom.iterable"], | ||
"module": "es6", | ||
"target": "es6", | ||
"allowJs": true, | ||
"outDir": "dist", | ||
"declaration": true, | ||
"strict": false | ||
"declaration": true | ||
}, | ||
"include": ["src"] | ||
"include": ["src", "src/index.js"] | ||
} |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
152200
19
1573
1
208
6
1