mirror of
https://github.com/konvajs/konva.git
synced 2025-04-05 20:48:28 +08:00
420 lines
11 KiB
HTML
420 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>KonvaJS text paths</title>
|
|
<meta
|
|
name="viewport"
|
|
content="width=device-width, initial-scale=1.0, user-scalable=1.0, minimum-scale=1.0, maximum-scale=1.0"
|
|
/>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
}
|
|
aside {
|
|
margin-left: 20px;
|
|
}
|
|
div {
|
|
padding: 2px;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<aside>
|
|
<div>
|
|
<label>
|
|
<span>Curvature</span>
|
|
<input type="range" value="0" id="radius" min="-100" max="100" />
|
|
<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>
|
|
|
|
<script type="module">
|
|
import Konva from '../src/index.ts';
|
|
|
|
const stage = new Konva.Stage({
|
|
container: 'container',
|
|
width: window.innerWidth,
|
|
height: window.innerHeight,
|
|
});
|
|
|
|
const layer = new Konva.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
|
|
// Credits to @opsb https://stackoverflow.com/a/18473154 for the polarToCartesian and describeArc functions
|
|
const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => ({
|
|
x: validate(
|
|
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, 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;
|
|
if (endAngleOriginal - startAngle === 360) {
|
|
endAngle = 359;
|
|
}
|
|
const start = polarToCartesian(x, y, validatedRadius, endAngle);
|
|
const end = polarToCartesian(x, y, validatedRadius, startAngle);
|
|
|
|
return {
|
|
data: [
|
|
'M',
|
|
start.x,
|
|
start.y,
|
|
'A',
|
|
validatedRadius,
|
|
validatedRadius,
|
|
1,
|
|
1,
|
|
sweep,
|
|
end.x,
|
|
end.y,
|
|
endAngleOriginal - startAngle === 360 ? 'z' : '',
|
|
].join(' '),
|
|
start,
|
|
radius: validatedRadius,
|
|
deltaAngle: endAngle - startAngle,
|
|
};
|
|
};
|
|
|
|
// create elements
|
|
const text = new Konva.TextPath({
|
|
text: 'Curved text',
|
|
align: 'center',
|
|
data: 'M 0 0',
|
|
fontSize: 20,
|
|
textBaseline: 'middle',
|
|
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({
|
|
stroke: 'black',
|
|
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
|
|
document
|
|
.querySelector('#align')
|
|
.addEventListener('change', ({ target: { value } }) => {
|
|
text.align(value);
|
|
correctAlignment();
|
|
correctRotation();
|
|
setPosition();
|
|
updateHelpersVisibility(false);
|
|
});
|
|
document
|
|
.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
|
|
.querySelector('#radius')
|
|
.addEventListener('input', ({ target: { value } }) => {
|
|
curvature = value;
|
|
correctAlignment();
|
|
correctRotation();
|
|
setPosition();
|
|
transformer.nodes([]);
|
|
|
|
updateHelpersVisibility(true);
|
|
|
|
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
|
|
layer.add(group);
|
|
|
|
layer.add(transformer);
|
|
</script>
|
|
</body>
|
|
</html>
|