KonvaJS how to change the line style? - konvajs

I'm trying to create a drawing tablet with a mouse.
With KonvaJS, I can draw a line or an arrow.
How do I change the style of the line and the end of the arrow?
I use Konva. Arrow and Konva.Line and that's what I need.
Example:

There are many ways to implement such shapes.
(1) You can use Konva.Path and generate data attribute with SVG path manually (from your mouse movements).
(2) You can use Konva.TextShape to draw arrow as a set of some symbols (like dots or dashes).
(3) You can use Custom Shape to draw with native 2d canvas API.
Here is a demo with all of them:
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const getAngle = (points) => {
const dx = points[1].x - points[0].x;
const dy = points[1].y - points[0].y;
const angle = Math.atan2(dy, dx);
return Konva.Util.radToDeg(angle);
}
const createPathArrow = () => {
return new Konva.Path({
x: 50,
y: 50,
stroke: 'red',
fill: 'red',
data: 'M 0 0 L 100 0 L 80 -10 L 90 0 L 80 10 L 100 0'
})
};
const createTextPathArrow = (points) => {
const [p1, p2] = points;
const group = new Konva.Group({
x: 50,
y: 150
});
group.add(new Konva.TextPath({
stroke: 'red',
text: '| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | ',
data: `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`
}));
group.add(new Konva.Text({
x: p2.x,
y: p2.y,
text: '➤',
fill: 'red',
fontSize: 25,
offsetY: 25 / 2,
offsetX: 25 / 2,
rotation: getAngle(points)
}))
return group;
};
const createSceneArrow = () => {
return new Konva.Shape({
x: 50,
y: 250,
stroke: 'red',
length: 100,
arcSize: 10,
sceneFunc: (ctx, shape) => {
const arcSize = shape.getAttr('arcSize');
const number = shape.getAttr('length') / arcSize;
ctx.beginPath();
for(var i = 0; i < number; i++) {
ctx.moveTo(i * arcSize, 0);
const midX = (i * arcSize + arcSize / 2);
const midY = 0;
if (i % 2 === 0) {
ctx.arc(midX, midY, arcSize / 2, Math.PI, 0, 1);
} else {
ctx.arc(midX, midY, arcSize / 2, Math.PI, 0);
}
}
ctx.fillStrokeShape(shape);
}
})
}
const points = [{x: 0, y: 0}, {x: 100, y: 30}];
layer.add(createPathArrow());
layer.add(createTextPathArrow(points));
layer.add(createSceneArrow())
<script src="https://unpkg.com/konva#^8/konva.min.js"></script>
<div id="container"></div>

Related

Is there a image pattern stroke option for lines?

I've been trying to look over the Konva shape library and haven't found a stroke reapeating pattern method. I've been trying to look for a way to implement https://stackoverflow.com/a/32323610/20557085 into the shape's sceneFunc, but ended up with a static version that keeps itself in the top right corner of the canvas at all times, even if the canvas/camera is moved/dragged.
The end-goal would be to have a image that repeats itself following a line's bezier curve of points, that I can change the width of.
The question would be if there is something I am missing that is already a part of Konva, or if I should continue to trial my way through the sceneFunc?
The class component used in my attempt, that ended up static:
import React, { Component } from 'react';
import { createRoot } from 'react-dom/client';
import { Stage, Layer, Image, Shape } from 'react-konva';
var PI = Math.PI;
class URLImageStroke extends React.Component {
constructor(props) {
super(props)
this.state = {
image: null,
points: [{ x: 0, y: 0 }, { x: 100, y: 100 }, { x: 150, y: 50 }, { x: 200, y: 200 }]
};
}
componentDidMount() {
this.loadImage();
this.getPoints()
}
loadImage() {
// save to "this" to remove "load" handler on unmount
this.image = new window.Image();
this.image.src = this.props.src;
this.image.addEventListener('progress', (e) => console.log(e))
this.image.addEventListener('load', this.handleLoad);
}
handleLoad = () => {
this.setState({
image: this.image,
});
};
getPoints = () => {
let points = [];
//for (let i = 0; this.state.points.length > i; i++) {
const s = this.state.points[0];
const c1 = this.state.points[1];
const c2 = this.state.points[2];
const e = this.state.points[3];
for (var t = 0; t <= 100; t += 0.25) {
var T = t / 100;
// plot a point on the curve
var pos = getCubicBezierXYatT(s, c1, c2, e, T);
// calculate the tangent angle of the curve at that point
var tx = bezierTangent(s.x, c1.x, c2.x, e.x, T);
var ty = bezierTangent(s.y, c1.y, c2.y, e.y, T);
var a = Math.atan2(ty, tx) - PI / 2;
// save the x/y position of the point and the tangent angle
// in the points array
points.push({
x: pos.x,
y: pos.y,
angle: a
});
}
this.setState({
points: points
});
}
render() {
return (
<Shape
x={50}
y={50}
width={this.props?.width}
height={this.props?.height}
image={this.state.image}
points={this.state?.points}
sceneFunc={(ctx, shape) => {
const img = shape.attrs.image;
if (!img) {
console.log("no image")
return;
}
const points = shape.attrs.points;
if (!points) {
console.log("no points")
return;
}
// Note: increase the lineWidth if
// the gradient has noticable gaps
ctx.lineWidth = 8;
ctx.strokeStyle = 'skyblue';
let sliceCount = 0;
// draw a gradient-stroked line tangent to each point on the curve
for (let i = 0; i < points.length; i++) {
let p = points[i];
ctx.translate(p.x, p.y);
ctx.rotate(p.angle - PI / 2);
// draw multiple times to fill gaps on outside of rope slices
ctx.drawImage(img, sliceCount, 0, 1, img.height, 0, 0, 1, img.height);
ctx.drawImage(img, sliceCount, 0, 1, img.height, 0, 0, 1, img.height);
ctx.drawImage(img, sliceCount, 0, 1, img.height, 0, 0, 1, img.height);
ctx.setTransform(1, 0, 0, 1, 0, 0);
++sliceCount;
if (sliceCount > (img.width - 1)) { sliceCount = 0; }
}
//ctx.strokeShape(this);
}
}
/>
);
}
}
//////////////////////////////////////////
// helper functions
//////////////////////////////////////////
// calculate one XY point along Cubic Bezier at interval T
// (where T==0.00 at the start of the curve and T==1.00 at the end)
function getCubicBezierXYatT(startPt, controlPt1, controlPt2, endPt, T) {
var x = CubicN(T, startPt.x, controlPt1.x, controlPt2.x, endPt.x);
var y = CubicN(T, startPt.y, controlPt1.y, controlPt2.y, endPt.y);
return ({ x: x, y: y });
}
// cubic helper formula at T distance
function CubicN(T, a, b, c, d) {
var t2 = T * T;
var t3 = t2 * T;
return a + (-a * 3 + T * (3 * a - a * T)) * T
+ (3 * b + T * (-6 * b + b * 3 * T)) * T
+ (c * 3 - c * 3 * T) * t2
+ d * t3;
}
// calculate the tangent angle at interval T on the curve
function bezierTangent(a, b, c, d, t) {
return (3 * t * t * (-a + 3 * b - 3 * c + d) + 6 * t * (a - 2 * b + c) + 3 * (-a + b));
};
export default URLImageStroke;

How can I simulate CSS like Object-Fit Cover in Konvajs React?

This is really easy in DOM because I can just do object-fit: cover in css and the image would crop and fill in the width and height. But in Konvajs the default seems to be to fill it.
Here is what I've tried:
const [image] = useImage(content.img);
if (image) {
image.setAttribute('style', `object-
fit:cover;width:${content.width};height:${content.height}`);
}
This doesn't work.
Just in case anyone else is looking for a solution.
The only way I could achieve this is doing calculations on where to crop the original image.
const crop = () => {
if (content.width > content.height) {
const [cropX, cropY, cropW, cropH] = cropBasedOnWidth();
if (cropY < 0) {
const [cropX, cropY, cropW, cropH] = cropBasedOnHeight();
return {x: cropX, y: cropY, height: cropH, width: cropW};
}
return {x: cropX, y: cropY, height: cropH, width: cropW};
} else if (content.width < content.height) {
const [cropX, cropY, cropW, cropH] = cropBasedOnHeight();
if (cropX < 0) {
const [cropX, cropY, cropW, cropH] = cropBasedOnWidth();
return {x: cropX, y: cropY, height: cropH, width: cropW};
}
return {x: cropX, y: cropY, height: cropH, width: cropW};
} else {
return undefined;
}
}
const cropBasedOnWidth = () => {
const cropW = content.naturalWidth;
const cropH = cropW / content.width * content.height;
const cropX = content.naturalWidth / 2 - cropW / 2;
const cropY = content.naturalHeight / 2 - cropH / 2;
return [cropX, cropY, cropW, cropH];
}
const cropBasedOnHeight = () => {
const cropH = content.naturalHeight;
const cropW = cropH / content.height * content.width;
const cropX = content.naturalWidth / 2 - cropW / 2;
const cropY = content.naturalHeight / 2 - cropH / 2;
return [cropX, cropY, cropW, cropH];
}
...
return <Image crop={crop()} ... />
I don't know if there is a better way.

Highcharts: How we can sort series with animation?

I am using horizontal bar chart with continuous update of series data. This is achieved successfully, but now i want these series to be sorted on data (continuously with every update of series) in desc order with animation. I mean when a bar get max value then move it to the top with animation.
how can i achieve this?
That type of functionality is not supported by default in Highcharts. Below you can find an example that shows how you can achieve the wanted result by custom code:
var options = {
chart: {
type: 'bar'
},
xAxis: {
categories: ['Cat1', 'Cat2', 'Cat3'],
},
series: [{
data: [1000, 900, 800]
}]
};
var chart = Highcharts.chart('container', options);
// Add custom data labels
chart.series[0].points.forEach(function(point, i) {
var x = chart.plotWidth - point.plotY + chart.plotLeft,
y = chart.xAxis[0].ticks[i].label.xy.y;
point.customDataLabel = chart.renderer.text(
point.y,
x,
y
)
.css({
color: '#000000',
fontSize: '14px'
})
.attr({
zIndex: 3
})
.add();
setAlign(point.customDataLabel);
});
function setAlign(label, xPos) {
var align = 'left',
bbox = label.getBBox();
if (chart.chartWidth < (xPos ? xPos : bbox.x + bbox.width) + 50) {
align = 'right';
}
label.attr({
align: align
})
}
var update = function() {
var points = chart.series[0].points;
chart.series[0].setData([Math.round(Math.random() * 1000), Math.round(Math.random() * 1000), Math.round(Math.random() * 1000)]);
};
var rotate = function() {
var points = chart.series[0].points,
labelX,
ticks = chart.xAxis[0].ticks;
var sortedPoints = points.slice();
sortedPoints.sort(function(a, b) {
return b.y - a.y;
});
points.forEach(function(point, i) {
sortedPoints.forEach(function(sPoint, j) {
if (point === sPoint) {
labelX = chart.plotWidth - points[i].plotY + chart.plotLeft;
// Animate the column
points[i].graphic.animate({
x: points[j].shapeArgs.x
});
// Animate the label
points[i].customDataLabel.attr({
text: points[i].y
}).animate({
y: ticks[j].label.xy.y,
x: labelX
});
setAlign(points[i].customDataLabel, labelX);
// Animate the axis label
ticks[i].label.animate({
y: ticks[j].label.xy.y
});
}
});
});
};
document.getElementById("button").addEventListener("click", function() {
update();
rotate();
}, false);
Live demo: https://jsfiddle.net/BlackLabel/mg5bv3s8/
API Reference: https://api.highcharts.com/class-reference/Highcharts.SVGElement#animate

Adjust igMap Marker Size

I am attempting to map out certain data points using ignite UI's igMap control. What I want to happen is based on the larger realized rate per hour, make the map marker larger or smaller. The documentation through infragistics doesn't seem to go into this very much, so if anyone has an input, I'd appreciate it
#model IEnumerable<OpsOverallGeoMapViewModel>
<style>
#tooltipTable {
font-family: Verdana, Arial, Helvetica, sans-serif;
width: 100%;
border-collapse: collapse;
}
#tooltipTable td, #tooltipTable th {
font-size: 9px;
border: 1px solid #28b51c;
padding: 3px 7px 2px 7px;
}
#tooltipTable th {
font-weight: bold;
font-size: 11px;
text-align: left;
padding-top: 5px;
padding-bottom: 4px;
background-color: #28b51c;
color: #ffffff;
}
</style>
<script id="tooltipTemplate" type="text/x-jquery-tmpl">
<table id="tooltipTable">
<tr><th class="tooltipHeading" colspan="2">${item.Country}</th></tr>
<tr>
<td>Total Hours:</td>
<td>${item.Hours}</td>
</tr>
<tr>
<td>Total Billing:</td>
<td>${item.Billing}</td>
</tr>
<tr>
<td>Realized Rate Per Hour:</td>
<td>${item.RealizedRatePerHour}</td>
</tr>
</table>
</script>
<div id="map"></div>
<script>
$(function () {
var model = #Html.Raw(Json.Encode(Model));
$("#map").igMap({
width: "700px",
height: "500px",
windowRect: { left: 0.225, top: 0.1, height: 0.6, width: 0.6 },
series: [{
type: "geographicSymbol",
name: "worldCities",
dataSource: model, //JSON Array defined above
latitudeMemberPath: "Latitude",
longitudeMemberPath: "Longitude",
markerType: "automatic",
markerOutline: "#28b51c",
markerBrush: "#28b51c",
showTooltip: true,
tooltipTemplate: "tooltipTemplate"
}],
});
});
</script>
<div id="map"></div>
I figured it out by running through the example for marker templates on infragistics website. By changing the circle radius of the marker, it makes this into a sort of heat map which is what I was looking for
$(function () {
var model = #Html.Raw(Json.Encode(Model.OrderBy(x => x.Billing)));
$("#map").igMap({
width: "700px",
height: "500px",
windowRect: { left: 0.1, top: 0.1, height: 0.7, width: 0.7 },
// specifies imagery tiles from BingMaps
backgroundContent: {
type: "bing",
key: "Masked Purposely",
imagerySet: "Road", // alternative: "Road" | "Aerial"
},
series: [{
type: "geographicSymbol",
name: "ratesGraph",
dataSource: model, //JSON Array defined above
latitudeMemberPath: "Latitude",
longitudeMemberPath: "Longitude",
markerType: "automatic",
markerCollisionAvoidance: "fade",
markerOutline: "#1142a6",
markerBrush: "#7197e5",
showTooltip: true,
tooltipTemplate: "customTooltip",
// Defines marker template rendering function
markerTemplate: {
measure: function (measureInfo) {
measureInfo.width = 10;
measureInfo.height = 10;
},
render: function (renderInfo) {
createMarker(renderInfo);
}
}
}]
});
});
function createMarker(renderInfo) {
var ctx = renderInfo.context;
var x = renderInfo.xPosition;
var y = renderInfo.yPosition;
var size = 10;
var heightHalf = size / 2.0;
var widthHalf = size / 2.0;
if (renderInfo.isHitTestRender) {
// This is called for tooltip hit test only
// Rough marker rectangle size calculation
ctx.fillStyle = renderInfo.data.actualItemBrush().fill();
ctx.fillRect(x - widthHalf, y - heightHalf, size, size);
} else {
var data = renderInfo.data;
var name = data.item()["CountryName"];
var type = data.item()["Country"];
var billing = data.item()["Billing"];
// Draw text
ctx.textBaseline = "top";
ctx.font = '8pt Verdana';
ctx.fillStyle = "black";
ctx.textBaseline = "middle";
wrapText(ctx, name, x + 3, y + 6, 80, 12);
// Draw marker
ctx.beginPath();
//SET THE CIRCLE RADIUS HERE*******
var circleRadius = 3;
var radiusFactor = billing / 100000;
if (radiusFactor > 4)
circleRadius = radiusFactor;
if (circleRadius > 10)
circleRadius = 10;
ctx.arc(x, y,circleRadius, 0, 2 * Math.PI, false);
ctx.fillStyle = "#36a815";
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = "black";
ctx.stroke();
}
}
// Plots a rectangle with rounded corners with a semi-transparent frame
function plotTextBackground(context, left, top, width, height) {
var cornerRadius = 3;
context.beginPath();
// Upper side and upper right corner
context.moveTo(left + cornerRadius, top);
context.lineTo(left + width - cornerRadius, top);
context.arcTo(left + width, top, left + width, top + cornerRadius, cornerRadius);
// Right side and lower right corner
context.lineTo(left + width, top + height - cornerRadius);
context.arcTo(left + width, top + height, left + width - cornerRadius, top + height, cornerRadius);
// Lower side and lower left corner
context.lineTo(left + cornerRadius, top + height);
context.arcTo(left, top + height, left, top + height - cornerRadius, cornerRadius);
// Left side and upper left corner
context.lineTo(left, top + cornerRadius);
context.arcTo(left, top, left + cornerRadius, top, cornerRadius);
// Fill white with 75% opacity
context.globalAlpha = 1;
context.fillStyle = "white";
context.fill();
context.globalAlpha = 1;
// Plot grey frame
context.lineWidth = 1;
context.strokeStyle = "grey";
context.stroke();
}
// Outputs text in a word wrapped fashion in a transparent frame
function wrapText(context, text, x, y, maxWidth, lineHeight) {
var words = text.split(" ");
var line = "";
var yCurrent = y;
var lines = [], currentLine = 0;
// Find the longest word in the text and update the max width if the longest word cannot fit
for (var i = 0; n < words.length; i++) {
var testWidth = context.measureText(words[i]);
if (testWidth > maxWidth)
maxWidth = metrics.width;
}
// Arrange all words into lines
for (var n = 0; n < words.length; n++) {
var testLine = line + words[n];
var testWidth = context.measureText(testLine).width;
if (testWidth > maxWidth) {
lines[currentLine] = line;
currentLine++;
line = words[n] + " ";
}
else {
line = testLine + " ";
}
}
lines[currentLine] = line;
// Plot frame and background
if (lines.length > 1) {
// Multiline text
plotTextBackground(context, x - 2, y - lineHeight / 2 - 2, maxWidth + 3, lines.length * lineHeight + 3);
}
else {
// Single line text
var textWidth = context.measureText(lines[0]).width; // Limit frame width to the actual line width
plotTextBackground(context, x - 2, y - lineHeight / 2 - 2, textWidth + 3, lines.length * lineHeight + 3);
}
// Output lines of text
context.fillStyle = "black";
for (var n = 0; n < lines.length; n++) {
context.fillText(" " + lines[n], x, yCurrent);
yCurrent += lineHeight;
}
}

KineticJS simple animation not working on mobile devices

I was wondering if someone could help me find the solution to this.
I've made a very simple animation using KineticJS.
All works perfect on desktop, unfortunately not on mobile devices (iPhone, iPad, Android).
Result is a slowish performance but most importantly distorted shapes.
I suspect it has something to do with resolution or viewport but am not sure.
Preview is on www.bartvanhelsdingen.com
Any suggestions are highly appreciated.
Below is the code:
var shapes = {
sizes: [30, 40, 50, 55, 60, 80],
gradients: [
[0, '#fdfaee', 1, '#524f43'],
[0, '#a39175', 1, '#dbae5e'],
[0, '#b4c188', 1, '#f3de7c'],
[0, '#eaf2ef', 1, '#587c71'],
[0, '#a39175', 1, '#dbae5e'],
[0, '#61845c', 1, '#b4b092']
],
},
dims = {
width: 300,
height: 500
},
stage = new Kinetic.Stage({
container: 'animation',
width: dims.width,
height: dims.height,
x: 0,
y: 0,
draggable: false
});
function getRandomColor() {
return colors[getRandomFromInterval(0, colors.length - 1)];
}
function getRandomGradient() {
return gradients[getRandomFromInterval(0, gradients.length - 1)];
}
function getRandomFromInterval(from, to) {
return Math.floor(Math.random() * (to - from + 1) + from);
}
function getRandomSpeed() {
var speed = getRandomFromInterval(1, 1);
return getRandomFromInterval(0, 1) ? speed : speed * -1;
}
function createGroup(x, y, size, strokeWidth) {
return new Kinetic.Group({
x: x,
y: y,
width: size,
height: size,
opacity: 0,
draggable: false,
clipFunc: function (canvas) {
var context = canvas.getContext();
context.beginPath();
context.moveTo(0, 0);
context.lineTo(0, size);
context.lineTo(size, size);
context.lineTo(size, 0);
context.rect(strokeWidth, strokeWidth, size - strokeWidth * 2, size - strokeWidth * 2);
}
});
}
function createShape(size, gradient, strokeWidth, cornerRadius) {
return new Kinetic.Rect({
x: 0,
y: 0,
width: size,
height: size,
fillLinearGradientStartPoint: [size, 0],
fillLinearGradientEndPoint: [size, size],
fillLinearGradientColorStops: gradient,
opacity: 1,
lineJoin: 'bevel',
strokeWidth: 0,
cornerRadius: cornerRadius
});
}
var layer = new Kinetic.Layer(),
animAttribs = [];
for (var n = 0; n < 6; n++) {
var size = shapes.sizes[n],
strokeWidth = Math.ceil(size * 0.12),
cornerRadius = Math.ceil(size * 0.04),
gradient = shapes.gradients[n],
x = getRandomFromInterval(size, dims.width) - size,
y = getRandomFromInterval(size, dims.height) - size;
var group = createGroup(x, y, size, strokeWidth);
var shape = createShape(size, gradient, strokeWidth, cornerRadius);
animAttribs.push({
nextChange: getRandomFromInterval(1, 3) * 1000,
startTime: 1000,
duration: 0,
x: getRandomSpeed(),
y: getRandomSpeed()
});
group.add(shape);
layer.add(group);
}
stage.add(layer);
anim = new Kinetic.Animation(function (frame) {
var time = frame.time,
timeDiff = frame.timeDiff,
frameRate = frame.frameRate;
for (var n = 0; n < layer.getChildren().length; n++) {
var shape = layer.getChildren()[n],
opacity = shape.getOpacity() + 0.01 > 1 ? 1 : shape.getOpacity() + 0.01,
attribs = animAttribs[n],
x, y;
if (attribs.duration >= attribs.nextChange) {
attribs.x = getRandomSpeed();
attribs.y = getRandomSpeed();
attribs.nextChange = getRandomFromInterval(3, 5) * 1000;
attribs.duration = 0;
}
if (time >= attribs.startTime) {
if (shape.getX() + attribs.x + shape.getWidth() >= stage.getWidth() || shape.getX() + attribs.x - shape.getWidth() / 2 <= 0) {
attribs.x *= -1;
}
if (shape.getY() + attribs.y + shape.getHeight() >= stage.getHeight() || shape.getY() + attribs.y - shape.getHeight() / 2 <= 0) {
attribs.y *= -1;
}
x = shape.getX() + attribs.x;
y = shape.getY() + attribs.y;
attribs.duration += timeDiff;
shape.setOpacity(opacity);
shape.setX(x);
shape.setY(y);
}
}
}, layer);
anim.start();
the problem you are facing is, that clipFunc isn't currently working on devices with pixelratio != 1.
This problem came up in this post as well. Eric Rowell, the creator of KineticJS added this issue to his release scedule for late September.
So there is nothing wrong with your animations, they're working as expected, but you can't see them because of the distorted clipping region
To resolve this issue "unofficially" you can simply replace the last line of the _clip function in your kinetic.js with the following: context.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); (credits for that go to Mark Smits)

Resources