mirror of
https://github.com/konvajs/konva.git
synced 2025-04-05 20:48:28 +08:00
feat: extend example, use static methods
This commit is contained in:
parent
a622479674
commit
0fea26b1c1
@ -2,7 +2,7 @@ import { Factory } from '../Factory';
|
|||||||
import { Shape, ShapeConfig } from '../Shape';
|
import { Shape, ShapeConfig } from '../Shape';
|
||||||
import { _registerNode } from '../Global';
|
import { _registerNode } from '../Global';
|
||||||
|
|
||||||
import { GetSet } from '../types';
|
import { GetSet, PathSegment } from '../types';
|
||||||
|
|
||||||
export interface PathConfig extends ShapeConfig {
|
export interface PathConfig extends ShapeConfig {
|
||||||
data?: string;
|
data?: string;
|
||||||
@ -33,20 +33,18 @@ export class Path extends Shape<PathConfig> {
|
|||||||
|
|
||||||
constructor(config?: PathConfig) {
|
constructor(config?: PathConfig) {
|
||||||
super(config);
|
super(config);
|
||||||
this.dataArray = Path.parsePathData(this.data());
|
this._readDataAttribute();
|
||||||
this.pathLength = 0;
|
|
||||||
for (var i = 0; i < this.dataArray.length; ++i) {
|
|
||||||
this.pathLength += this.dataArray[i].pathLength;
|
|
||||||
}
|
|
||||||
this.on('dataChange.konva', function () {
|
this.on('dataChange.konva', function () {
|
||||||
this.dataArray = Path.parsePathData(this.data());
|
this._readDataAttribute();
|
||||||
this.pathLength = 0;
|
|
||||||
for (var i = 0; i < this.dataArray.length; ++i) {
|
|
||||||
this.pathLength += this.dataArray[i].pathLength;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_readDataAttribute() {
|
||||||
|
this.dataArray = Path.parsePathData(this.data());
|
||||||
|
this.pathLength = Path.getPathLength(this.dataArray);
|
||||||
|
}
|
||||||
|
|
||||||
_sceneFunc(context) {
|
_sceneFunc(context) {
|
||||||
var ca = this.dataArray;
|
var ca = this.dataArray;
|
||||||
|
|
||||||
@ -215,21 +213,39 @@ export class Path extends Shape<PathConfig> {
|
|||||||
* var point = path.getPointAtLength(10);
|
* var point = path.getPointAtLength(10);
|
||||||
*/
|
*/
|
||||||
getPointAtLength(length) {
|
getPointAtLength(length) {
|
||||||
|
return Path.getPointAtLengthOfDataArray(length, this.dataArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
data: GetSet<string, this>;
|
||||||
|
|
||||||
|
static getLineLength(x1, y1, x2, y2) {
|
||||||
|
return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
|
||||||
|
}
|
||||||
|
|
||||||
|
static getPathLength(dataArray: PathSegment[]) {
|
||||||
|
let pathLength = 0;
|
||||||
|
for (var i = 0; i < dataArray.length; ++i) {
|
||||||
|
pathLength += dataArray[i].pathLength;
|
||||||
|
}
|
||||||
|
return pathLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getPointAtLengthOfDataArray(length: number, dataArray) {
|
||||||
var point,
|
var point,
|
||||||
i = 0,
|
i = 0,
|
||||||
ii = this.dataArray.length;
|
ii = dataArray.length;
|
||||||
|
|
||||||
if (!ii) {
|
if (!ii) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (i < ii && length > this.dataArray[i].pathLength) {
|
while (i < ii && length > dataArray[i].pathLength) {
|
||||||
length -= this.dataArray[i].pathLength;
|
length -= dataArray[i].pathLength;
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i === ii) {
|
if (i === ii) {
|
||||||
point = this.dataArray[i - 1].points.slice(-2);
|
point = dataArray[i - 1].points.slice(-2);
|
||||||
return {
|
return {
|
||||||
x: point[0],
|
x: point[0],
|
||||||
y: point[1],
|
y: point[1],
|
||||||
@ -237,14 +253,14 @@ export class Path extends Shape<PathConfig> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (length < 0.01) {
|
if (length < 0.01) {
|
||||||
point = this.dataArray[i].points.slice(0, 2);
|
point = dataArray[i].points.slice(0, 2);
|
||||||
return {
|
return {
|
||||||
x: point[0],
|
x: point[0],
|
||||||
y: point[1],
|
y: point[1],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var cp = this.dataArray[i];
|
var cp = dataArray[i];
|
||||||
var p = cp.points;
|
var p = cp.points;
|
||||||
switch (cp.command) {
|
switch (cp.command) {
|
||||||
case 'L':
|
case 'L':
|
||||||
@ -286,11 +302,6 @@ export class Path extends Shape<PathConfig> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
data: GetSet<string, this>;
|
|
||||||
|
|
||||||
static getLineLength(x1, y1, x2, y2) {
|
|
||||||
return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
|
|
||||||
}
|
|
||||||
static getPointOnLine(dist, P1x, P1y, P2x, P2y, fromX?, fromY?) {
|
static getPointOnLine(dist, P1x, P1y, P2x, P2y, fromX?, fromY?) {
|
||||||
if (fromX === undefined) {
|
if (fromX === undefined) {
|
||||||
fromX = P1x;
|
fromX = P1x;
|
||||||
@ -406,7 +417,7 @@ export class Path extends Shape<PathConfig> {
|
|||||||
* L data for the purpose of high performance Path
|
* L data for the purpose of high performance Path
|
||||||
* rendering
|
* rendering
|
||||||
*/
|
*/
|
||||||
static parsePathData(data) {
|
static parsePathData(data): PathSegment[] {
|
||||||
// Path Data Segment must begin with a moveTo
|
// Path Data Segment must begin with a moveTo
|
||||||
//m (x y)+ Relative moveTo (subsequent points are treated as lineTo)
|
//m (x y)+ Relative moveTo (subsequent points are treated as lineTo)
|
||||||
//M (x y)+ Absolute moveTo (subsequent points are treated as lineTo)
|
//M (x y)+ Absolute moveTo (subsequent points are treated as lineTo)
|
||||||
|
@ -74,7 +74,7 @@ function _strokeFunc(context) {
|
|||||||
export class TextPath extends Shape<TextPathConfig> {
|
export class TextPath extends Shape<TextPathConfig> {
|
||||||
dummyCanvas = Util.createCanvasElement();
|
dummyCanvas = Util.createCanvasElement();
|
||||||
dataArray = [];
|
dataArray = [];
|
||||||
path: SVGPathElement | Path;
|
path: SVGPathElement | undefined;
|
||||||
glyphInfo: Array<{
|
glyphInfo: Array<{
|
||||||
transposeX: number;
|
transposeX: number;
|
||||||
transposeY: number;
|
transposeY: number;
|
||||||
@ -84,6 +84,7 @@ export class TextPath extends Shape<TextPathConfig> {
|
|||||||
p1: Vector2d;
|
p1: Vector2d;
|
||||||
}>;
|
}>;
|
||||||
partialText: string;
|
partialText: string;
|
||||||
|
pathLength: number;
|
||||||
textWidth: number;
|
textWidth: number;
|
||||||
textHeight: number;
|
textHeight: number;
|
||||||
|
|
||||||
@ -107,19 +108,51 @@ export class TextPath extends Shape<TextPathConfig> {
|
|||||||
this._setTextData();
|
this._setTextData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getTextPathLength() {
|
||||||
|
// defines the length of the path
|
||||||
|
// if possible use native browser method, otherwise use KonvaJS implementation
|
||||||
|
if (typeof window !== 'undefined' && this.attrs.data) {
|
||||||
|
try {
|
||||||
|
if (!this.path) {
|
||||||
|
this.path = document.createElementNS(
|
||||||
|
'http://www.w3.org/2000/svg',
|
||||||
|
'path'
|
||||||
|
) as SVGPathElement;
|
||||||
|
this.path.setAttribute('d', this.attrs.data);
|
||||||
|
}
|
||||||
|
return this.path.getTotalLength();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
return Path.getPathLength(this.dataArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.getPathLength(this.dataArray);
|
||||||
|
}
|
||||||
|
_getPointAtLength(length: number) {
|
||||||
|
// if path is not defined yet, do nothing
|
||||||
|
if (!this.attrs.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if possible use native browser method, otherwise use KonvaJS implementation
|
||||||
|
if (typeof window !== 'undefined' && this.attrs.data && this.path) {
|
||||||
|
try {
|
||||||
|
return this.path.getPointAtLength(length);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
// try using KonvaJS implementation as a backup
|
||||||
|
return Path.getPointAtLengthOfDataArray(length, this.dataArray);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Path.getPointAtLengthOfDataArray(length, this.dataArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_readDataAttribute() {
|
_readDataAttribute() {
|
||||||
this.dataArray = Path.parsePathData(this.attrs.data);
|
this.dataArray = Path.parsePathData(this.attrs.data);
|
||||||
// in case document is not defined (server side rendering)
|
this.path = undefined;
|
||||||
// use the KonvaJs Path class instead of the browser's native SVGPathElement
|
this.pathLength = this._getTextPathLength();
|
||||||
if (typeof window !== 'undefined' && document) {
|
|
||||||
this.path = document.createElementNS(
|
|
||||||
'http://www.w3.org/2000/svg',
|
|
||||||
'path'
|
|
||||||
) as SVGPathElement;
|
|
||||||
this.path.setAttribute('d', this.attrs.data);
|
|
||||||
} else {
|
|
||||||
this.path = new Path({ data: this.attrs.data }) as Path;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_sceneFunc(context) {
|
_sceneFunc(context) {
|
||||||
@ -223,7 +256,7 @@ export class TextPath extends Shape<TextPathConfig> {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
width: metrics.width,
|
width: metrics.width,
|
||||||
height: parseInt(this.attrs.fontSize, 10),
|
height: parseInt(`${this.fontSize()}`, 10),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_setTextData() {
|
_setTextData() {
|
||||||
@ -246,43 +279,16 @@ export class TextPath extends Shape<TextPathConfig> {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
// defines the length of the path
|
|
||||||
// if possible use native browser method, otherwise use KonvaJS implementation
|
|
||||||
const pathLength =
|
|
||||||
(this.path as SVGPathElement).getTotalLength?.() ||
|
|
||||||
(this.path as Path).getLength?.();
|
|
||||||
|
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
if (align === 'center') {
|
if (align === 'center') {
|
||||||
offset = Math.max(0, pathLength / 2 - textWidth / 2);
|
offset = Math.max(0, this.pathLength / 2 - textWidth / 2);
|
||||||
}
|
}
|
||||||
if (align === 'right') {
|
if (align === 'right') {
|
||||||
offset = Math.max(0, pathLength - textWidth);
|
offset = Math.max(0, this.pathLength - textWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
const charArr = stringToArray(this.text());
|
const charArr = stringToArray(this.text());
|
||||||
|
|
||||||
const getPointAtLength = (length: number) => {
|
|
||||||
// if path is not defined yet, do nothing
|
|
||||||
if (!this.attrs.data) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if possible use native browser method, otherwise use KonvaJS implementation
|
|
||||||
if (typeof window !== 'undefined' && this.attrs.data) {
|
|
||||||
try {
|
|
||||||
return this.path.getPointAtLength(length);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e);
|
|
||||||
// try using KonvaJS implementation as a backup
|
|
||||||
this.path = new Path({ data: this.attrs.data });
|
|
||||||
return this.path.getPointAtLength(length);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return this.path.getPointAtLength(length);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Algorithm for calculating glyph positions:
|
// Algorithm for calculating glyph positions:
|
||||||
// 1. Get the begging point of the glyph on the path using the offsetToGlyph,
|
// 1. Get the begging point of the glyph on the path using the offsetToGlyph,
|
||||||
// 2. Get the ending point of the glyph on the path using the offsetToGlyph plus glyph width,
|
// 2. Get the ending point of the glyph on the path using the offsetToGlyph plus glyph width,
|
||||||
@ -290,16 +296,16 @@ export class TextPath extends Shape<TextPathConfig> {
|
|||||||
// 4. Add glyph width to the offsetToGlyph and repeat
|
// 4. Add glyph width to the offsetToGlyph and repeat
|
||||||
let offsetToGlyph = offset;
|
let offsetToGlyph = offset;
|
||||||
for (var i = 0; i < charArr.length; i++) {
|
for (var i = 0; i < charArr.length; i++) {
|
||||||
const charStartPoint = getPointAtLength(offsetToGlyph);
|
const charStartPoint = this._getPointAtLength(offsetToGlyph);
|
||||||
if (!charStartPoint) return;
|
if (!charStartPoint) return;
|
||||||
|
|
||||||
let glyphWidth = this._getTextSize(charArr[i]).width + letterSpacing;
|
let glyphWidth = this._getTextSize(charArr[i]).width + letterSpacing;
|
||||||
if (charArr[i] === ' ' && align === 'justify') {
|
if (charArr[i] === ' ' && align === 'justify') {
|
||||||
const numberOfSpaces = this.text().split(' ').length - 1;
|
const numberOfSpaces = this.text().split(' ').length - 1;
|
||||||
glyphWidth += (pathLength - textWidth) / numberOfSpaces;
|
glyphWidth += (this.pathLength - textWidth) / numberOfSpaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
const charEndPoint = getPointAtLength(offsetToGlyph + glyphWidth);
|
const charEndPoint = this._getPointAtLength(offsetToGlyph + glyphWidth);
|
||||||
|
|
||||||
const width = Path.getLineLength(
|
const width = Path.getLineLength(
|
||||||
charStartPoint.x,
|
charStartPoint.x,
|
||||||
|
27
src/types.ts
27
src/types.ts
@ -8,6 +8,33 @@ export interface Vector2d {
|
|||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PathSegment {
|
||||||
|
command:
|
||||||
|
| 'm'
|
||||||
|
| 'M'
|
||||||
|
| 'l'
|
||||||
|
| 'L'
|
||||||
|
| 'v'
|
||||||
|
| 'V'
|
||||||
|
| 'h'
|
||||||
|
| 'H'
|
||||||
|
| 'z'
|
||||||
|
| 'Z'
|
||||||
|
| 'c'
|
||||||
|
| 'C'
|
||||||
|
| 'q'
|
||||||
|
| 'Q'
|
||||||
|
| 't'
|
||||||
|
| 'T'
|
||||||
|
| 's'
|
||||||
|
| 'S'
|
||||||
|
| 'a'
|
||||||
|
| 'A';
|
||||||
|
start: Vector2d;
|
||||||
|
points: Vector2d[];
|
||||||
|
pathLength: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IRect {
|
export interface IRect {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
@ -13,16 +13,67 @@
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
aside {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<input type="range" value="100" id="radius" min="1" max="1000" />
|
<aside>
|
||||||
<select id="align">
|
<div>
|
||||||
<option value="center">center</option>
|
<label>
|
||||||
<option value="left">left</option>
|
<span>Curvature</span>
|
||||||
<option value="right">right</option>
|
<input type="range" value="0" id="radius" min="-100" max="100" />
|
||||||
</select>
|
<input type="number" value="0" id="curvature" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<span>Font size</span>
|
||||||
|
<input type="range" value="0" id="fontsize" min="1" max="100" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<span>Alignment</span>
|
||||||
|
<select id="align">
|
||||||
|
<option value="center">center</option>
|
||||||
|
<option value="left">left</option>
|
||||||
|
<option value="right">right</option>
|
||||||
|
</select></label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<span>Font weight</span>
|
||||||
|
<select id="fontweight">
|
||||||
|
<option value="normal">normal</option>
|
||||||
|
<option value="bold">bold</option>
|
||||||
|
<option value="italic">italic</option>
|
||||||
|
</select></label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<span>Text decoration</span>
|
||||||
|
<select id="textdecoration">
|
||||||
|
<option value="">none</option>
|
||||||
|
<option value="line-through">line-through</option>
|
||||||
|
<option value="underline">underline</option>
|
||||||
|
</select></label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<span>Text</span>
|
||||||
|
<textarea id="textinput">Curved text</textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<div id="container"></div>
|
<div id="container"></div>
|
||||||
|
|
||||||
@ -38,20 +89,56 @@
|
|||||||
const layer = new Konva.Layer();
|
const layer = new Konva.Layer();
|
||||||
stage.add(layer);
|
stage.add(layer);
|
||||||
|
|
||||||
|
// define variables
|
||||||
|
let alignXShift = 0;
|
||||||
|
let alignYShift = 0;
|
||||||
|
let curvature = 0;
|
||||||
|
let helpersTimeout;
|
||||||
|
|
||||||
|
// define constants
|
||||||
|
const RADIUS = 50;
|
||||||
|
let shiftX = 400;
|
||||||
|
let shiftY = 200;
|
||||||
|
const RANGE = 100000;
|
||||||
|
|
||||||
|
const getRadius = (currentValue) =>
|
||||||
|
Math.abs(1 / Math.tan(currentValue / 100)) * RADIUS;
|
||||||
|
|
||||||
|
const isOutOfRange = () => validate(getRadius(curvature)) === RANGE;
|
||||||
|
const getArcSweep = () => (Math.sign(curvature) >= 0 ? 0 : 1);
|
||||||
|
|
||||||
|
const validate = (number) => {
|
||||||
|
return Math.max(-RANGE, Math.min(number, RANGE));
|
||||||
|
};
|
||||||
|
|
||||||
// define arc calculation
|
// define arc calculation
|
||||||
// Credits to @opsb https://stackoverflow.com/a/18473154 for the polarToCartesian and describeArc functions
|
// Credits to @opsb https://stackoverflow.com/a/18473154 for the polarToCartesian and describeArc functions
|
||||||
const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => ({
|
const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => ({
|
||||||
x: centerX + radius * Math.cos(((angleInDegrees - 90) * Math.PI) / 180),
|
x: validate(
|
||||||
y: centerY + radius * Math.sin(((angleInDegrees - 90) * Math.PI) / 180),
|
centerX + radius * Math.cos(((angleInDegrees - 90) * Math.PI) / 180)
|
||||||
|
),
|
||||||
|
y: validate(
|
||||||
|
centerY + radius * Math.sin(((angleInDegrees - 90) * Math.PI) / 180)
|
||||||
|
),
|
||||||
});
|
});
|
||||||
const describeArc = (x, y, radius, startAngle, endAngle) => {
|
const describeArc = (x, y, radius, startAngle, endAngle, sweep) => {
|
||||||
|
const validatedRadius = validate(radius);
|
||||||
|
|
||||||
|
if (isOutOfRange()) {
|
||||||
|
const width = text.getTextWidth();
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: `M ${-width / 2} 0 L ${width / 2} 0`,
|
||||||
|
start: { x, y },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const endAngleOriginal = endAngle;
|
const endAngleOriginal = endAngle;
|
||||||
if (endAngleOriginal - startAngle === 360) {
|
if (endAngleOriginal - startAngle === 360) {
|
||||||
endAngle = 359;
|
endAngle = 359;
|
||||||
}
|
}
|
||||||
const start = polarToCartesian(x, y, radius, endAngle);
|
const start = polarToCartesian(x, y, validatedRadius, endAngle);
|
||||||
const end = polarToCartesian(x, y, radius, startAngle);
|
const end = polarToCartesian(x, y, validatedRadius, startAngle);
|
||||||
const arcSweep = endAngle - startAngle <= 180 ? '0' : '1';
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: [
|
data: [
|
||||||
@ -59,77 +146,274 @@
|
|||||||
start.x,
|
start.x,
|
||||||
start.y,
|
start.y,
|
||||||
'A',
|
'A',
|
||||||
radius,
|
validatedRadius,
|
||||||
radius,
|
validatedRadius,
|
||||||
0,
|
1,
|
||||||
arcSweep,
|
1,
|
||||||
0,
|
sweep,
|
||||||
end.x,
|
end.x,
|
||||||
end.y,
|
end.y,
|
||||||
endAngleOriginal - startAngle === 360 ? 'z' : '',
|
endAngleOriginal - startAngle === 360 ? 'z' : '',
|
||||||
].join(' '),
|
].join(' '),
|
||||||
start,
|
start,
|
||||||
|
radius: validatedRadius,
|
||||||
|
deltaAngle: endAngle - startAngle,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// define constants
|
|
||||||
const { data, start } = describeArc(0, 0, 100, 0, 360);
|
|
||||||
const shiftX = 400;
|
|
||||||
const shiftY = 200;
|
|
||||||
const x = shiftX + start.x;
|
|
||||||
const y = shiftX + start.y;
|
|
||||||
|
|
||||||
// create elements
|
// create elements
|
||||||
const text = new Konva.TextPath({
|
const text = new Konva.TextPath({
|
||||||
x,
|
text: 'Curved text',
|
||||||
y,
|
|
||||||
text: 'Curved text with Konva.TextPath',
|
|
||||||
align: 'center',
|
align: 'center',
|
||||||
data,
|
data: 'M 0 0',
|
||||||
|
fontSize: 20,
|
||||||
|
textBaseline: 'middle',
|
||||||
fill: 'black',
|
fill: 'black',
|
||||||
});
|
});
|
||||||
|
const group = new Konva.Group({ draggable: true });
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
const transformer = new Konva.Transformer({
|
||||||
|
resizeEnabled: true,
|
||||||
|
rotateEnabled: true,
|
||||||
|
shouldOverdrawWholeArea: true,
|
||||||
|
});
|
||||||
|
const positioner = new Konva.Rect({
|
||||||
|
fill: 'green',
|
||||||
|
opacity: 0.1,
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
|
const circleCenter = new Konva.Rect({
|
||||||
|
fill: 'red',
|
||||||
|
opacity: 0.1,
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
const path = new Konva.Path({
|
const path = new Konva.Path({
|
||||||
data,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
stroke: 'black',
|
stroke: 'black',
|
||||||
opacity: 0.3,
|
opacity: 0.3,
|
||||||
|
visible: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getArcData = () =>
|
||||||
|
describeArc(0, 0, getRadius(curvature), 0, 360, getArcSweep());
|
||||||
|
|
||||||
|
// create methods to calculate positions
|
||||||
|
const calculateTextPlacement = () => {
|
||||||
|
const { data, start } = getArcData();
|
||||||
|
const x = shiftX + start.x - alignXShift;
|
||||||
|
const y = shiftY + start.y - alignYShift;
|
||||||
|
|
||||||
|
text.data(data);
|
||||||
|
text.x(x);
|
||||||
|
text.y(y);
|
||||||
|
};
|
||||||
|
const calculatePositionerPlacement = () => {
|
||||||
|
const { start } = getArcData();
|
||||||
|
|
||||||
|
positioner.x(
|
||||||
|
text.x() - start.x - text.getTextWidth() / 2 + alignXShift
|
||||||
|
);
|
||||||
|
positioner.y(
|
||||||
|
text.y() - start.y - text.getTextHeight() / 2 + alignYShift
|
||||||
|
);
|
||||||
|
positioner.width(text.getTextWidth());
|
||||||
|
positioner.height(text.getTextHeight());
|
||||||
|
};
|
||||||
|
const calculateCircleCenterPlacement = () => {
|
||||||
|
const { start } = getArcData();
|
||||||
|
|
||||||
|
let centerShiftY = 0;
|
||||||
|
if (getArcSweep() === 1) {
|
||||||
|
const alignment = text.align();
|
||||||
|
if (alignment !== 'center') {
|
||||||
|
centerShiftY = -start.y * 2;
|
||||||
|
} else {
|
||||||
|
centerShiftY = start.y * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = shiftX + start.x - alignXShift;
|
||||||
|
const y = shiftY + start.y - alignYShift + centerShiftY;
|
||||||
|
|
||||||
|
circleCenter.x(x - 2);
|
||||||
|
circleCenter.y(y - 2);
|
||||||
|
circleCenter.width(5);
|
||||||
|
circleCenter.height(5);
|
||||||
|
};
|
||||||
|
const calculatePathPlacement = () => {
|
||||||
|
const { data, start } = getArcData();
|
||||||
|
const x = shiftX + start.x - alignXShift;
|
||||||
|
const y = shiftY + start.y - alignYShift;
|
||||||
|
|
||||||
|
path.data(data);
|
||||||
|
|
||||||
|
path.x(x);
|
||||||
|
path.y(y);
|
||||||
|
};
|
||||||
|
|
||||||
|
// calculate initial positions
|
||||||
|
calculateTextPlacement();
|
||||||
|
calculatePathPlacement();
|
||||||
|
calculatePositionerPlacement();
|
||||||
|
|
||||||
|
// update positions on change
|
||||||
|
const setPosition = () => {
|
||||||
|
calculateTextPlacement();
|
||||||
|
calculatePathPlacement();
|
||||||
|
calculatePositionerPlacement();
|
||||||
|
calculateCircleCenterPlacement();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateHelpersVisibility = (showHelpers) => {
|
||||||
|
if (helpersTimeout) {
|
||||||
|
clearTimeout(helpersTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// force transformer update
|
||||||
|
if (transformer.nodes().length > 0) {
|
||||||
|
transformer.nodes([group]);
|
||||||
|
}
|
||||||
|
|
||||||
|
helpersTimeout = setTimeout(() => {
|
||||||
|
updateHelpersVisibility(false);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
positioner.visible(showHelpers);
|
||||||
|
circleCenter.visible(showHelpers);
|
||||||
|
path.visible(showHelpers);
|
||||||
|
};
|
||||||
|
|
||||||
|
// create methods to correct rotation and alignment
|
||||||
|
const correctRotation = () => {
|
||||||
|
const value = text.align();
|
||||||
|
if (isOutOfRange()) {
|
||||||
|
text.rotation(0);
|
||||||
|
path.rotation(0);
|
||||||
|
} else if (value === 'right') {
|
||||||
|
text.rotation(180);
|
||||||
|
path.rotation(180);
|
||||||
|
} else if (value === 'left') {
|
||||||
|
text.rotation(180);
|
||||||
|
path.rotation(180);
|
||||||
|
} else {
|
||||||
|
text.rotation(0);
|
||||||
|
path.rotation(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const correctAlignment = () => {
|
||||||
|
const { start, radius, deltaAngle } = getArcData();
|
||||||
|
const value = text.align();
|
||||||
|
if (isOutOfRange()) {
|
||||||
|
alignXShift = 0;
|
||||||
|
} else if (value === 'right') {
|
||||||
|
alignXShift = -text.getTextWidth() / 2;
|
||||||
|
} else if (value === 'left') {
|
||||||
|
alignXShift = text.getTextWidth() / 2;
|
||||||
|
} else {
|
||||||
|
alignXShift = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === 'center' && getArcSweep() === 1) {
|
||||||
|
alignYShift = start.y * 4;
|
||||||
|
alignXShift = start.x * 2;
|
||||||
|
} else {
|
||||||
|
alignYShift = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// attach handlers
|
// attach handlers
|
||||||
document
|
document
|
||||||
.querySelector('#align')
|
.querySelector('#align')
|
||||||
.addEventListener('change', ({ target: { value } }) => {
|
.addEventListener('change', ({ target: { value } }) => {
|
||||||
text.align(value);
|
text.align(value);
|
||||||
window.text = text;
|
correctAlignment();
|
||||||
if (value === 'right') {
|
correctRotation();
|
||||||
text.rotation(180);
|
setPosition();
|
||||||
} else if (value === 'left') {
|
updateHelpersVisibility(false);
|
||||||
text.rotation(-180);
|
});
|
||||||
} else {
|
document
|
||||||
text.rotation(0);
|
.querySelector('#fontweight')
|
||||||
}
|
.addEventListener('change', ({ target: { value } }) => {
|
||||||
|
text.fontStyle(value);
|
||||||
|
updateHelpersVisibility(false);
|
||||||
|
});
|
||||||
|
document
|
||||||
|
.querySelector('#textinput')
|
||||||
|
|
||||||
|
.addEventListener('input', ({ target: { value } }) => {
|
||||||
|
text.text(value);
|
||||||
|
setPosition();
|
||||||
|
updateHelpersVisibility(false);
|
||||||
|
});
|
||||||
|
document
|
||||||
|
.querySelector('#textdecoration')
|
||||||
|
.addEventListener('change', ({ target: { value } }) => {
|
||||||
|
text.textDecoration(value);
|
||||||
|
updateHelpersVisibility(false);
|
||||||
|
});
|
||||||
|
document
|
||||||
|
.querySelector('#fontsize')
|
||||||
|
.addEventListener('input', ({ target: { value } }) => {
|
||||||
|
text.fontSize(Number(value));
|
||||||
|
setPosition();
|
||||||
|
updateHelpersVisibility(false);
|
||||||
});
|
});
|
||||||
document
|
document
|
||||||
.querySelector('#radius')
|
.querySelector('#radius')
|
||||||
.addEventListener('input', ({ target: { value } }) => {
|
.addEventListener('input', ({ target: { value } }) => {
|
||||||
const { data, start } = describeArc(0, 0, value, 0, 360);
|
curvature = value;
|
||||||
const x = shiftX + start.x;
|
correctAlignment();
|
||||||
const y = shiftX + start.y;
|
correctRotation();
|
||||||
|
setPosition();
|
||||||
|
transformer.nodes([]);
|
||||||
|
|
||||||
text.data(data);
|
updateHelpersVisibility(true);
|
||||||
path.data(data);
|
|
||||||
text.x(x);
|
|
||||||
path.x(x);
|
|
||||||
text.y(y);
|
|
||||||
path.y(y);
|
|
||||||
|
|
||||||
layer.draw();
|
document.querySelector('#curvature').value = value;
|
||||||
|
});
|
||||||
|
document
|
||||||
|
.querySelector('#curvature')
|
||||||
|
.addEventListener('input', ({ target: { value } }) => {
|
||||||
|
curvature = value;
|
||||||
|
correctAlignment();
|
||||||
|
correctRotation();
|
||||||
|
setPosition();
|
||||||
|
updateHelpersVisibility(true);
|
||||||
|
transformer.nodes([]);
|
||||||
|
|
||||||
|
document.querySelector('#radius').value = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// attach handlers to konva elements
|
||||||
|
group.on('click', (e) => {
|
||||||
|
updateHelpersVisibility(false);
|
||||||
|
transformer.nodes([group]);
|
||||||
|
});
|
||||||
|
group.on('dragmove', (e) => {
|
||||||
|
updateHelpersVisibility(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
group.on('transform', (e) => {
|
||||||
|
updateHelpersVisibility(false);
|
||||||
|
});
|
||||||
|
stage.on('click', (e) => {
|
||||||
|
if (e.target === stage) {
|
||||||
|
transformer.nodes([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHelpersVisibility(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// keep curved text in a group
|
||||||
|
group.add(text);
|
||||||
|
group.add(path);
|
||||||
|
// group.add(positioner);
|
||||||
|
group.add(circleCenter);
|
||||||
|
|
||||||
// add the shapes to the layer
|
// add the shapes to the layer
|
||||||
layer.add(text);
|
layer.add(group);
|
||||||
layer.add(path);
|
|
||||||
|
layer.add(transformer);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
Reference in New Issue
Block a user