Chart.js Picture inside doughnut segment - javascript

I found out about chart.js and
I am looking to use a doughnut chart for my website, I found a example where I can take the basics from : https://jsfiddle.net/9wp4f693/2/
I've only found something like this, but it was to draw text inside the segments, not to add pictures.
function drawSegmentValues()
{
for(var i=0; i<myDoughnutChart.segments.length; i++)
{
ctx.fillStyle="white";
var textSize = myChart.width/10;
ctx.font= textSize+"px Verdana";
// Get needed variables
var value = myDoughnutChart.segments[i].value;
var startAngle = myDoughnutChart.segments[i].startAngle;
var endAngle = myDoughnutChart.segments[i].endAngle;
var middleAngle = startAngle + ((endAngle - startAngle)/2);
// Compute text location
var posX = (radius/2) * Math.cos(middleAngle) + midX;
var posY = (radius/2) * Math.sin(middleAngle) + midY;
// Text offside by middle
var w_offset = ctx.measureText(value).width/2;
var h_offset = textSize/4;
ctx.fillText(value, posX - w_offset, posY + h_offset);
}
}
But I would like to have pictures inside my segments, something like this but I have no clue how I would do this :

There is no native ChartJS API for drawing an image inside a donut chart.
But you can manually add the images after the chart has been drawn.
For each wedge in the donut:
Warning: untested code ... some tweaking might be required
Translate inward to the middle of the donut.
// calculate donut center (cx,cy) & translate to it
var cx=chart.width/2;
var cy=chart.height/2;
context.translate(cx,cy);
Rotate to the mid-angle of the target donut-wedge
var startAngle = chart.segments[thisWedgeIndex].startAngle;
var endAngle = chart.segments[thisWedgeIndex].endAngle;
var midAngle = startAngle+(endAngle-startAngle)/2;
// rotate by the midAngle
context.rotate(midAngle);
Translate outward to the midpoint of the target donut-wedge:
// given the donut radius (innerRadius) and donut radius (radius)
var midWedgeRadius=chart.innerRadius+(chart.radius-chart.innerRadius)/2;
context.translate(midWedgeRadius,0);
Draw the image offset by half the image width & height:
// given the image width & height
context.drawImage(theImage,-theImage.width/2,-theImage.height/2);
Clean up the transformations by resetting the transform matrix to default:
// undo translate & rotate
context.setTransform(1,0,0,1,0,0);

In the new version use the following example, (it requires chartjs-plugin-labels):
import React from 'react';
import { Doughnut } from 'react-chartjs-2';
import 'chartjs-plugin-labels';
const imageURLs = [
'https://avatars.githubusercontent.com/u/43679262?v=4',
'https://avatars.githubusercontent.com/u/43679262?v=4',
'https://avatars.githubusercontent.com/u/43679262?v=4',
];
const images = imageURLs.map((v) => {
var image = new Image();
image.src = v;
return image;
});
export const data_doughnut = {
labels: ['a', 'b', 'c'],
datasets: [
{
data: [30, 15, 10],
backgroundColor: [
'#B1A9FF',
'#877CF8',
'#6456F2',
],
weight: 1,
},
],
};
export const chartOptions = {
responsive: true,
plugins: {
legend: {
display: false,
},
},
scales: {
ticks: {
display: false,
},
},
};
export const plugins = [
{
afterDatasetsDraw: (chart) => {
var ctx = chart.ctx;
ctx.save();
var xCenter = chart.canvas.width / 2;
var yCenter = chart.canvas.height / 2;
var data = chart.config.data.datasets[0].data;
var vTotal = data.reduce((a, b) => a + b, 0);
data.forEach((v, i) => {
var vAngle =
data.slice(0, i).reduce((a, b) => a + b, 0) + v / 2;
var angle = (360 / vTotal) * vAngle - 90;
var radians = angle * (Math.PI / 180);
var r = yCenter;
// modify position
var x = xCenter + (Math.cos(radians) * r) / 1.4;
var y = yCenter + (Math.sin(radians) * r) / 1.4;
ctx.translate(x, y);
var image = images[i];
ctx.drawImage(image, -image.width / 2, -image.height / 2);
ctx.translate(-x, -y);
});
ctx.restore();
},
},
];
export function DoughnutChartFeekers() {
return (
<Doughnut
data={data_doughnut}
plugins={plugins}
options={chartOptions}
/>
);
}

Related

How to add two borders for point in chart line using Chart.js v4.2.1

I have this chart. line chart using chartjs
How can I draw the last point, I'm using chart.js v4.2.1 and javascript. I have tried this code and it didn't work.
function createRadialGradient3(context, c1, c2, c3) {
const chartArea = context.chart.chartArea;
if (!chartArea) {
// This case happens on initial chart load
return;
}
const chartWidth = chartArea.right - chartArea.left;
const chartHeight = chartArea.bottom - chartArea.top;
if (width !== chartWidth || height !== chartHeight) {
cache.clear();
}
let gradient = cache.get(c1 + c2 + c3);
if (!gradient) {
// Create the gradient because this is either the first render
// or the size of the chart has changed
width = chartWidth;
height = chartHeight;
const centerX = (chartArea.left + chartArea.right) / 2;
const centerY = (chartArea.top + chartArea.bottom) / 2;
const r = Math.min(
(chartArea.right - chartArea.left) / 2,
(chartArea.bottom - chartArea.top) / 2
);
const ctx = context.chart.ctx;
gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, r);
gradient.addColorStop(0, c1);
gradient.addColorStop(0.5, c2);
gradient.addColorStop(1, c3);
cache.set(c1 + c2 + c3, gradient);
}
return gradient;
}

How to smoothly scroll repeating linear gradient on canvas 2D?

I’m using context.createLinearGradient to create gradients, and to make it scroll I'm animating the colorStops. But the issue is when a color reaches the end, if I wrap it around back to start the whole gradient changes.
In CSS I could avoid this using repeating-linear-gradient and it would work but I havent figured out a way to do this without the sudden color changes at the edges. I tried drawing it a little bit offscreen but It still off.
This is what I have so far:
const colors = [
{ color: "#FF0000", pos: 0 },
{ color: "#FFFF00", pos: 1 / 5 },
{ color: "#00FF00", pos: 2 / 5 },
{ color: "#0000FF", pos: 3 / 5 },
{ color: "#FF00FF", pos: 4 / 5 },
{ color: "#FF0000", pos: 1 },
];
const angleStep = 0.2;
const linearStep = 0.001;
function init() {
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
const mw = canvas.width;
const mh = canvas.height;
let angle = 0;
function drawScreen() {
angle = (angle + angleStep) % 360;
const [x1, y1, x2, y2] = angleToPoints(angle, mw, mh);
const gradient = context.createLinearGradient(x1, y1, x2, y2);
for (const colorStop of colors) {
gradient.addColorStop(colorStop.pos, colorStop.color);
colorStop.pos += linearStep;
if (colorStop.pos > 1) colorStop.pos = 0;
}
context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
}
function loop() {
drawScreen()
window.requestAnimationFrame(loop);
}
loop();
}
function angleToPoints(angle, width, height){
const rad = ((180 - angle) / 180) * Math.PI;
// This computes the length such that the start/stop points will be at the corners
const length = Math.abs(width * Math.sin(rad)) + Math.abs(height * Math.cos(rad));
// Compute the actual x,y points based on the angle, length of the gradient line and the center of the div
const halfx = (Math.sin(rad) * length) / 2.0
const halfy = (Math.cos(rad) * length) / 2.0
const cx = width / 2.0
const cy = height / 2.0
const x1 = cx - halfx
const y1 = cy - halfy
const x2 = cx + halfx
const y2 = cy + halfy
return [x1, y1, x2, y2];
}
init();
html,body, canvas {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
<canvas width="128" height="72"></canvas>
The problem is that the gradients you create don't usually have stops at 0 or 1. When a gradient doesn't have those stops, the ends get filled out by whatever the color is of the closest stop.
To fill them in the way you want, you'd need to figure out what the color at the crossover point should be and add it to both ends.
Below, we determine the current end colors by sorting and then use linear interpolation (lerp) to get the crossover color. I've prefixed my meaningful changes with comments that start with // ###.
// ### lerp for hexadecimal color strings
function lerpColor(a, b, amount) {
const
ah = +a.replace('#', '0x'),
ar = ah >> 16,
ag = ah >> 8 & 0xff,
ab = ah & 0xff,
bh = +b.replace('#', '0x'),
br = bh >> 16,
bg = bh >> 8 & 0xff,
bb = bh & 0xff,
rr = ar + amount * (br - ar),
rg = ag + amount * (bg - ag),
rb = ab + amount * (bb - ab)
;
return '#' + (0x1000000 + (rr << 16) + (rg << 8) + rb | 0).toString(16).slice(1);
}
const colors = [
{ color: "#FF0000", pos: 0 },
{ color: "#FFFF00", pos: 1 / 5 },
{ color: "#00FF00", pos: 2 / 5 },
{ color: "#0000FF", pos: 3 / 5 },
{ color: "#FF00FF", pos: 4 / 5 },
{ color: "#FF0000", pos: 1 },
];
const angleStep = 0.2;
const linearStep = 0.005;
function init() {
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
const mw = canvas.width;
const mh = canvas.height;
let angle = 0;
function drawScreen() {
angle = (angle + angleStep) % 360;
const [x1, y1, x2, y2] = angleToPoints(angle, mw, mh);
const gradient = context.createLinearGradient(x1, y1, x2, y2);
for (const colorStop of colors) {
gradient.addColorStop(colorStop.pos, colorStop.color);
colorStop.pos += linearStep;
// ### corrected error here
if (colorStop.pos > 1) colorStop.pos -= 1;
}
// ### compute and set the gradient end stops
const sortedStops = colors.sort((a,b) => a.pos - b.pos);
const firstStop = sortedStops[0];
const lastStop = sortedStops.slice(-1)[0];
const endColor = lerpColor(firstStop.color, lastStop.color, firstStop.pos*5);
gradient.addColorStop(0, endColor);
gradient.addColorStop(1, endColor);
context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
}
function loop() {
drawScreen()
requestAnimationFrame(loop)
}
loop();
}
function angleToPoints(angle, width, height){
const rad = ((180 - angle) / 180) * Math.PI;
// This computes the length such that the start/stop points will be at the corners
const length = Math.abs(width * Math.sin(rad)) + Math.abs(height * Math.cos(rad));
// Compute the actual x,y points based on the angle, length of the gradient line and the center of the div
const halfx = (Math.sin(rad) * length) / 2.0
const halfy = (Math.cos(rad) * length) / 2.0
const cx = width / 2.0
const cy = height / 2.0
const x1 = cx - halfx
const y1 = cy - halfy
const x2 = cx + halfx
const y2 = cy + halfy
return [x1, y1, x2, y2];
}
init();
html, body, canvas { width: 100%; height: 100%; margin: 0; padding: 0; }
<canvas width="128" height="72"></canvas>

How to track coordinates on the quadraticCurve

I have a Polyline on the HiDPICanvas (html5 canvas). When I move mouse left and right I track its coordinates and on corresponding point with same X coordinate on the polyline I draw a Circle. You can try it now to see the result.
// Create a canvas
var HiDPICanvas = function(container_id, color, w, h) {
/*
objects are objects on the canvas, first elements of dictionary are background elements, last are on the foreground
canvas will be placed in the container
canvas will have width w and height h
*/
var objects = {
box : [],
borders : [],
circles : [],
polyline: []
}
var doNotMove = ['borders']
// is mouse down & its coords
var mouseDown = false
lastX = window.innerWidth/2
lastY = window.innerHeight/2
// return pixel ratio
var getRatio = function() {
var ctx = document.createElement("canvas").getContext("2d");
var dpr = window.devicePixelRatio || 1;
var bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
return dpr / bsr;
}
// return high dots per inch canvas
var createHiDPICanvas = function() {
var ratio = getRatio();
var chart_container = document.getElementById(container_id);
var can = document.createElement("canvas");
can.style.backgroundColor = color
can.width = w * ratio;
can.height = h * ratio;
can.style.width = w + "px";
can.style.height = h + "px";
can.getContext("2d").setTransform(ratio, 0, 0, ratio, 0, 0);
chart_container.appendChild(can);
return can;
}
// add object to the canvas
var add = function(object, category) {
objects[category].push(object)
}
// clear canvas
var clearCanvas = function(x0, y0, x1, y1) {
ctx.clearRect(x0, y0, x1, y1);
ctx.beginPath();
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
ctx.closePath();
}
// check function do I can move this group of objects
var canMove = function(groupname) {
for (var i = 0; i < doNotMove.length; i++) {
var restricted = doNotMove[i]
if (restricted == groupname) {
return false
}
}
return true
}
// refresh all objects on the canvas
var refresh = function() {
clearCanvas(0, 0, w, h)
var object
for (var key in objects) {
for (var i = 0; i < objects[key].length; i++) {
object = objects[key][i]
object.refresh()
}
}
}
// shift all objects on the canvas except left and down borders and its content
var shiftObjects = function(event) {
event.preventDefault()
// if mouse clicked now -> we can move canvas view left\right
if (mouseDown) {
var object
for (var key in objects) {
if (canMove(key)) {
for (var i = 0; i < objects[key].length; i++) {
object = objects[key][i]
object.move(event.movementX, event.movementY)
}
}
}
cci.refresh()
}
}
// transfer x to canvas drawing zone x coord (for not drawing on borders of the canvas)
var transferX = function(x) {
return objects.borders[0].width + x
}
var transferCoords = function(x, y) {
// no need to transfer y because borders are only at the left
return {
x : transferX(x),
y : y
}
}
// change mouse state on the opposite
var toggleMouseState = function() {
mouseDown = !mouseDown
}
// make mouseDown = false, (bug removal function when mouse down & leaving the canvas)
var refreshMouseState = function() {
mouseDown = false
}
// print information about all objects on the canvas
var print = function() {
var groupLogged = true
console.log("Objects on the canvas:")
for (var key in objects) {
groupLogged = !groupLogged
if (!groupLogged) {console.log(key, ":"); groupLogged = !groupLogged}
for (var i = 0 ; i < objects[key].length; i++) {
console.log(objects[key][i])
}
}
}
var restrictEdges = function() {
console.log("offsetLeft", objects['borders'][0])
}
var getMouseCoords = function() {
return {
x : lastX,
y : lastY
}
}
var addCircleTracker = function() {
canvas.addEventListener("mousemove", (e) => {
var polyline = objects.polyline[0]
var mouseCoords = getMouseCoords()
var adjNodes = polyline.get2NeighbourNodes(mouseCoords.x)
if (adjNodes != -1) {
var prevNode = adjNodes.prev
var currNode = adjNodes.curr
var cursorNode = polyline.linearInterpolation(prevNode, currNode, mouseCoords.x)
// cursorNode.cursorX, cursorNode.cursorY are coords
// for circle that should be drawn on the polyline
// between the closest neighbour nodes
var circle = objects.circles[0]
circle.changePos(cursorNode.x, cursorNode.y)
refresh()
}
})
}
// create canvas
var canvas = createHiDPICanvas()
addCircleTracker()
// we created canvas so we can track mouse coords
var trackMouse = function(event) {
lastX = event.offsetX || (event.pageX - canvas.offsetLeft)
lastY = event.offsetY || (event.pageY - canvas.offsetTop)
}
// 2d context
var ctx = canvas.getContext("2d")
// add event listeners to the canvas
canvas.addEventListener("mousemove" , shiftObjects )
canvas.addEventListener("mousemove", (e) =>{ trackMouse(e) })
canvas.addEventListener("mousedown" , () => { toggleMouseState () })
canvas.addEventListener("mouseup" , () => { toggleMouseState () })
canvas.addEventListener("mouseleave", () => { refreshMouseState() })
canvas.addEventListener("mouseenter", () => { refreshMouseState() })
return {
// base objects
canvas : canvas,
ctx : ctx,
// sizes of the canvas
width : w,
height : h,
color : color,
// add object on the canvas for redrawing
add : add,
print : print,
// refresh canvas
refresh: refresh,
// objects on the canvas
objects: objects,
// get mouse coords
getMouseCoords : getMouseCoords
}
}
// cci -> canvas ctx info (dict)
var cci = HiDPICanvas("lifespanChart", "bisque", 780, 640)
var ctx = cci.ctx
var canvas = cci.canvas
var Polyline = function(path, color) {
var create = function() {
if (this.path === undefined) {
this.path = path
this.color = color
}
ctx.save()
ctx.beginPath()
p = this.path
ctx.fillStyle = color
ctx.moveTo(p[0].x, p[0].y)
for (var i = 0; i < p.length - 1; i++) {
var currentNode = p[i]
var nextNode = p[i+1]
// draw smooth polyline
// var xc = (currentNode.x + nextNode.x) / 2;
// var yc = (currentNode.y + nextNode.y) / 2;
// taken from https://stackoverflow.com/a/7058606/13727076
// ctx.quadraticCurveTo(currentNode.x, currentNode.y, xc, yc);
// draw rough polyline
ctx.lineTo(currentNode.x, currentNode.y)
}
ctx.stroke()
ctx.restore()
ctx.closePath()
}
// circle that will track mouse coords and be
// on the corresponding X coord on the path
// following mouse left\right movements
var circle = new Circle(50, 50, 5, "purple")
cci.add(circle, "circles")
create()
var get2NeighbourNodes = function(x) {
// x, y are cursor coords on the canvas
//
// Get 2 (left and right) neighbour nodes to current cursor x,y
// N are path nodes, * is Node we search coords for
//
// N-----------*----------N
//
for (var i = 1; i < this.path.length; i++) {
var prevNode = this.path[i-1]
var currNode = this.path[i]
if ( prevNode.x <= x && currNode.x >= x ) {
return {
prev : prevNode,
curr : currNode
}
}
}
return -1
}
var linearInterpolation = function(prevNode, currNode, cursorX) {
// calculate x, y for the node between 2 nodes
// on the path using linearInterpolation
// https://en.wikipedia.org/wiki/Linear_interpolation
var cursorY = prevNode.y + (cursorX - prevNode.x) * ((currNode.y - prevNode.y)/(currNode.x - prevNode.x))
return {
x : cursorX,
y : cursorY
}
}
var move = function(diff_x, diff_y) {
for (var i = 0; i < this.path.length; i++) {
this.path[i].x += diff_x
this.path[i].y += diff_y
}
}
return {
create : create,
refresh: create,
move : move,
get2NeighbourNodes : get2NeighbourNodes,
linearInterpolation : linearInterpolation,
path : path,
color : color
}
}
var Circle = function(x, y, radius, fillStyle) {
var create = function() {
if (this.x === undefined) {
this.x = x
this.y = y
this.radius = radius
this.fillStyle = fillStyle
}
ctx.save()
ctx.beginPath()
ctx.arc(this.x, this.y, radius, 0, 2*Math.PI)
ctx.fillStyle = fillStyle
ctx.strokeStyle = fillStyle
ctx.fill()
ctx.stroke()
ctx.closePath()
ctx.restore()
}
create()
var changePos = function(new_x, new_y) {
this.x = new_x
this.y = new_y
}
var move = function(diff_x, diff_y) {
this.x += diff_x
this.y += diff_y
}
return {
refresh : create,
create : create,
changePos: changePos,
move : move,
radius : radius,
x : this.x,
y : this.y
}
}
var Node = function(x, y) {
this.x = x
this.y = y
return {
x : this.x,
y : this.y
}
}
var poly = new Polyline([
Node(30,30), Node(150,150),
Node(290, 150), Node(320,200),
Node(350,350), Node(390, 250),
Node(450, 140)
], "green")
cci.add(poly, "polyline")
<div>
<div id="lifespanChart"></div>
</div>
But if you go to the comment draw smooth polyline and uncomment code below (and comment line that draws rough polyline) - it will draw smooth polyline now (quadratic Bézier curve). But when you try to move mouse left and right - Circle sometimes goes out of polyline bounds.
before quadratic curve:
after quadratic curve:
Here is a question : I calculated x, y coordinates for the Circle on the rough polyline using linear interpolation, but how could I calculate x, y coordinates for the Circle on the smooth quadratic curve?
ADD 1 : QuadraticCurve using Beizer curve as a base in calculations when smoothing polyline
ADD 2 For anyone who a little stucked with the implementation I found & saved easier solution from here, example:
var canvas = document.getElementById("canv")
var canvasRect = canvas.getBoundingClientRect()
var ctx = canvas.getContext('2d')
var p0 = {x : 30, y : 30}
var p1 = {x : 20, y :100}
var p2 = {x : 200, y :100}
var p3 = {x : 200, y :20}
// Points are objects with x and y properties
// p0: start point
// p1: handle of start point
// p2: handle of end point
// p3: end point
// t: progression along curve 0..1
// returns an object containing x and y values for the given t
// link https://stackoverflow.com/questions/14174252/how-to-find-out-y-coordinate-of-specific-point-in-bezier-curve-in-canvas
var BezierCubicXY = function(p0, p1, p2, p3, t) {
var ret = {};
var coords = ['x', 'y'];
var i, k;
for (i in coords) {
k = coords[i];
ret[k] = Math.pow(1 - t, 3) * p0[k] + 3 * Math.pow(1 - t, 2) * t * p1[k] + 3 * (1 - t) * Math.pow(t, 2) * p2[k] + Math.pow(t, 3) * p3[k];
}
return ret;
}
var draw_poly = function () {
ctx.beginPath()
ctx.lineWidth=2
ctx.strokeStyle="white"
ctx.moveTo(p0.x, p0.y)// start point
// cont cont end
ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)
ctx.stroke()
ctx.closePath()
}
var clear_canvas = function () {
ctx.clearRect(0,0,300,300);
ctx.beginPath();
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
ctx.closePath();
};
var draw_circle = function(x, y) {
ctx.save();
// semi-transparent arua around the circle
ctx.globalCompositeOperation = "source-over";
ctx.beginPath()
ctx.fillStyle = "white"
ctx.strokeStyle = "white"
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.stroke();
ctx.closePath();
ctx.restore();
}
var refresh = function(circle_x, circle_y) {
clear_canvas()
draw_circle(circle_x, circle_y)
draw_poly()
}
var dist = function(mouse, point) {
return Math.abs(mouse.x - point.x)
// return ((mouse.x - point.x)**2 + (mouse.y - point.y)**2)**0.5
}
var returnClosest = function(curr, prev) {
if (curr < prev) {
return curr
}
return prev
}
refresh(30,30)
canvas.addEventListener("mousemove", (e) => {
var mouse = {
x : e.clientX - canvasRect.left,
y : e.clientY - canvasRect.top
}
var Point = BezierCubicXY(p0, p1, p2, p3, 0)
for (var t = 0; t < 1; t += 0.01) {
var nextPoint = BezierCubicXY(p0, p1, p2, p3, t)
if (dist(mouse, Point) > dist(mouse, nextPoint)) {
Point = nextPoint
}
// console.log(Point)
}
refresh(Point.x, Point.y)
})
canvas {
background: grey;
}
<canvas id="canv" width = 300 height = 300></canvas>
Just iterate through all the lines of the curve & find closest position using this pattern
This can be done using an iterative search, as you have done with the lines.
BTW there is a much better way to find the closest point on a line that has a complexity of O(1) rather than O(n) where n is length of line segment.
Search for closest point
The following function can be used for both quadratic and cubic beziers and returns the unit position of closest point on bezier to a given coordinate.
The function also has a property foundPoint that has the position of the point found
The function uses the object Point that defines a 2D coordinate.
Signatures
The function has two signatures, one for quadratic beziers and the other for cubic.
closestPointOnBezier(point, resolution, p1, p2, cp1)
closestPointOnBezier(point, resolution, p1, p2, cp1, cp2)
Where
point as Point is the position to check
resolution as Number The approx resolution to search the bezier. If 0 then this is fixed to DEFAULT_SCAN_RESOLUTION else it is the distance between start and end points times resolution IE if resolution = 1 then approx scan is 1px, if resolution = 2 then approx scan is 1/2px
p1, p2 as Point's are the start and end points of the bezier
cp1, cp2 as Point's are the first and/or second control points of the bezier
Results
They both return Number that is the unit pos on the bezier of closest point. The value will be 0 <= result <= 1 Where 0 is at start of bezier and 1 is end
The function property closestPointOnBezier.foundPoint as Point has the coordinate of the closest point on the bezier and can be used to calculate the distance to the point on the bezier.
The function
const Point = (x = 0, y = 0) => ({x, y});
const MAX_RESOLUTION = 2048;
const DEFAULT_SCAN_RESOLUTION = 256;
closestPointOnBezier.foundPoint = Point();
function closestPointOnBezier(point, resolution, p1, p2, cp1, cp2) {
var unitPos, a, b, b1, c, i, vx, vy, closest = Infinity;
const v1 = Point(p1.x - point.x, p1.y - point.y);
const v2 = Point(p2.x - point.x, p2.y - point.y);
const v3 = Point(cp1.x - point.x, cp1.y - point.y);
resolution = resolution > 0 && reolution < MAX_RESOLUTION ? (Math.hypot(p1.x - p2.x, p1.y - p2.y) + 1) * resolution : 100;
const fp = closestPointOnBezier.foundPoint;
const step = 1 / resolution;
const end = 1 + step / 2;
const checkClosest = (e = (vx * vx + vy * vy) ** 0.5) => {
if (e < closest ){
unitPos = i;
closest = e;
fp.x = vx;
fp.y = vy;
}
}
if (cp2 === undefined) { // find quadratic
for (i = 0; i <= end; i += step) {
a = (1 - i);
c = i * i;
b = a*2*i;
a *= a;
vx = v1.x * a + v3.x * b + v2.x * c;
vy = v1.y * a + v3.y * b + v2.y * c;
checkClosest();
}
} else { // find cubic
const v4 = Point(cp2.x - point.x, cp2.y - point.y);
for (i = 0; i <= end; i += step) {
a = (1 - i);
c = i * i;
b = 3 * a * a * i;
b1 = 3 * c * a;
a = a * a * a;
c *= i;
vx = v1.x * a + v3.x * b + v4.x * b1 + v2.x * c;
vy = v1.y * a + v3.y * b + v4.y * b1 + v2.y * c;
checkClosest();
}
}
return unitPos < 1 ? unitPos : 1; // unit pos on bezier. clamped
}
Usage
Example usage to find closest point on two beziers
The defined geometry
const bzA = {
p1: Point(10, 100), // start point
p2: Point(200, 400), // control point
p3: Point(410, 500), // end point
};
const bzB = {
p1: bzA.p3, // start point
p2: Point(200, 400), // control point
p3: Point(410, 500), // end point
};
const mouse = Point(?,?);
Finding closest
// Find first point
closestPointOnBezier(mouse, 2, bzA.p1, bzA.p3, bzA.p2);
// copy point
var found = Point(closestPointOnBezier.foundPoint.x, closestPointOnBezier.foundPoint.y);
// get distance to mouse
var dist = Math.hypot(found.x - mouse.x, found.y - mouse.y);
// find point on second bezier
closestPointOnBezier(mouse, 2, bzB.p1, bzB.p3, bzB.p2);
// get distance of second found point
const distB = Math.hypot(closestPointOnBezier.foundPoint.x - mouse.x, closestPointOnBezier.foundPoint.y - mouse.y);
// is closer
if (distB < dist) {
found.x = closestPointOnBezier.foundPoint.x;
found.y = closestPointOnBezier.foundPoint.y;
dist = distB;
}
The closet point is found as Point

Rotate a cube to be isometric

I'm following this rotating cube tutorial and I'm trying to rotate the cube to an isometric perspective (45 degrees, 30 degrees).
The problem is, I think, is that the rotateY and rotateX functions alter the original values such that the two red dots in the middle of the cube (visually) don't overlap. (If that makes any sense)
How can I rotate the cube on it's X and Y axis at the same time so the functions don't effect each other?
const canvas = document.getElementById('stage');
canvas.width = canvas.parentElement.clientWidth
canvas.height = canvas.parentElement.clientHeight
const context = canvas.getContext('2d');
context.translate(200,200)
var node0 = [-100, -100, -100];
var node1 = [-100, -100, 100];
var node2 = [-100, 100, -100];
var node3 = [-100, 100, 100];
var node4 = [ 100, -100, -100];
var node5 = [ 100, -100, 100];
var node6 = [ 100, 100, -100];
var node7 = [ 100, 100, 100];
var nodes = [node0, node1, node2, node3, node4, node5, node6, node7];
var edge0 = [0, 1];
var edge1 = [1, 3];
var edge2 = [3, 2];
var edge3 = [2, 0];
var edge4 = [4, 5];
var edge5 = [5, 7];
var edge6 = [7, 6];
var edge7 = [6, 4];
var edge8 = [0, 4];
var edge9 = [1, 5];
var edge10 = [2, 6];
var edge11 = [3, 7];
var edges = [edge0, edge1, edge2, edge3, edge4, edge5, edge6, edge7, edge8, edge9, edge10, edge11];
var draw = function(){
for (var e=0; e<edges.length; e++){
var n0 = edges[e][0]
var n1 = edges[e][1]
var node0 = nodes[n0];
var node1 = nodes[n1];
context.beginPath();
context.moveTo(node0[0],node0[1]);
context.lineTo(node1[0],node1[1]);
context.stroke();
}
//draw nodes
for (var n=0; n<nodes.length; n++){
var node = nodes[n];
context.beginPath();
context.arc(node[0], node[1], 3, 0, 2 * Math.PI, false);
context.fillStyle = 'red';
context.fill();
}
}
var rotateZ3D = function(theta){
var sin_t = Math.sin(theta);
var cos_t = Math.cos(theta);
for (var n=0; n< nodes.length; n++){
var node = nodes[n];
var x = node[0];
var y = node[1];
node[0] = x * cos_t - y * sin_t;
node[1] = y * cos_t + x * sin_t;
};
};
var rotateY3D = function(theta){
var sin_t = Math.sin(theta);
var cos_t = Math.cos(theta);
for (var n=0; n<nodes.length; n++){
var node = nodes[n];
var x = node[0];
var z = node[2];
node[0] = x * cos_t - z * sin_t;
node[2] = z * cos_t + x * sin_t;
}
};
var rotateX3D = function(theta){
var sin_t = Math.sin(theta);
var cos_t = Math.cos(theta);
for (var n = 0; n< nodes.length; n++){
var node = nodes[n];
var y = node[1];
var z = node[2];
node[1] = y * cos_t - z * sin_t;
node[2] = z * cos_t + y * sin_t;
}
}
rotateY3D(Math.PI/4);
rotateX3D(Math.PI/6);
draw();
#stage {
background-color: cyan;
}
<canvas id="stage" height='500px' width='500px'></canvas>
Edit: I should have included a picture to further explain what I'm trying to achieve. I have a room picture that is isometric (45°,30°) and I'm overlaying it with a canvas so that I can draw the cube on it. As you can see it's slightly off, and I think its the effect of two compounding rotations since each function alters the original node coordinates.
You want projection not rotation
Your problem is that you are trying to apply a projection but using a transformation matrix to do it.
The transformation matrix will keep the box true to its original shape, with each axis at 90 deg to the others.
You want to have one axis at 45deg and the other at 30deg. You can not do that with rotations alone.
Projection matrix
The basic 3 by 4 matrix represents 4 3D vectors. These vectors are the direction and scale of the x,y,z axis in 3D space and the 4th vector is the origin.
The projection matrix removes the z part converting coordinates to 2D space. The z part of each axis is 0.
As the isometric projection is parallel we can just create a matrix that sets the 2D axis directions on the canvas.
The axis
The xAxis at 45 deg
const xAxis = Math.PI * ( 1 / 4);
iso.x.set(Math.cos(xAxis), Math.sin(xAxis), 0);
The yAxis at 120 deg
const yAxis = Math.PI * ( 4 / 6);
iso.y.set(Math.cos(yAxis), Math.sin(yAxis), 0);
And also the z axis which is up the page
iso.z.set(0,-1,0);
The transformation
Then we just multiply each vertex coord by the appropriate axis
// m is the matrix (iso)
// a is vertex in
// b is vertex out
// m.o is origin (not used in this example
b.x = a.x * m.x.x + a.y * m.y.x + a.z * m.z.x + m.o.x;
b.y = a.x * m.x.y + a.y * m.y.y + a.z * m.z.y + m.o.y;
b.z = a.x * m.x.z + a.y * m.y.z + a.z * m.z.z + m.o.z;
// ^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^
// move x dist move y dist move z dist
// along x axis along y axis along y axis
// 45deg 120deg Up -90deg
An example of above code
I have laid out a very basic Matrix in the snippet for reference.
The snippet creates 3D object using your approx layout.
The transform needs a second object for the result
I also added a projectIso that takes the directions of x,y,z axis and the scale of the x,y,z axis and creates a projection matrix as outlined above.
So thus the above is done with
const mat = Mat().projectIso(
Math.PI * ( 1 / 4),
Math.PI * ( 4 / 6),
Math.PI * ( 3 / 2) // up
); // scales default to 1
const ctx = canvas.getContext('2d');
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
const V = (x,y,z) => ({x,y,z,set(x,y,z){this.x = x;this.y = y; this.z = z}});
const Mat = () => ( {
x : V(1,0,0),
y : V(0,1,0),
z : V(0,0,1),
o : V(0,0,0), // origin
ident(){
const m = this;
m.x.set(1,0,0);
m.y.set(0,1,0);
m.z.set(0,0,1);
m.o.set(0,0,0);
return m;
},
rotX(r) {
const m = this.ident();
m.y.set(0, Math.cos(r), Math.sin(r));
m.z.set(0, -Math.sin(r), Math.cos(r));
return m;
},
rotY(r) {
const m = this.ident();
m.x.set(Math.cos(r), 0, Math.sin(r));
m.z.set(-Math.sin(r), 0, Math.cos(r));
return m;
},
rotZ(r) {
const m = this.ident();
m.x.set(Math.cos(r), Math.sin(r), 0);
m.y.set(-Math.sin(r), Math.cos(r), 0);
return m;
},
projectIso(xAxis, yAxis, zAxis, xScale = 1, yScale = 1, zScale = 1) {
const m = this.ident();
iso.x.set(Math.cos(xAxis) * xScale, Math.sin(xAxis) * xScale, 0);
iso.y.set(Math.cos(yAxis) * yScale, Math.sin(yAxis) * yScale, 0);
iso.z.set(Math.cos(zAxis) * zScale, Math.sin(zAxis) * zScale, 0);
return m;
},
transform(obj, result){
const m = this;
const na = obj.nodes;
const nb = result.nodes;
var i = 0;
while(i < na.length){
const a = na[i];
const b = nb[i++];
b.x = a.x * m.x.x + a.y * m.y.x + a.z * m.z.x + m.o.x;
b.y = a.x * m.x.y + a.y * m.y.y + a.z * m.z.y + m.o.y;
b.z = a.x * m.x.z + a.y * m.y.z + a.z * m.z.z + m.o.z;
}
return result;
}
});
// create a box
const Box = (size = 35) =>( {
nodes: [
V(-size, -size, -size),
V(-size, -size, size),
V(-size, size, -size),
V(-size, size, size),
V(size, -size, -size),
V(size, -size, size),
V(size, size, -size),
V(size, size, size),
],
edges: [[0, 1],[1, 3],[3, 2],[2, 0],[4, 5],[5, 7],[7, 6],[6, 4],[0, 4],[1, 5],[2, 6],[3, 7]],
});
// draws a obj that has nodes, and edges
function draw(obj) {
ctx.fillStyle = 'red';
const edges = obj.edges;
const nodes = obj.nodes;
var i = 0;
ctx.beginPath();
while(i < edges.length){
var edge = edges[i++];
ctx.moveTo(nodes[edge[0]].x, nodes[edge[0]].y);
ctx.lineTo(nodes[edge[1]].x, nodes[edge[1]].y);
}
ctx.stroke();
i = 0;
ctx.beginPath();
while(i < nodes.length){
const x = nodes[i].x;
const y = nodes[i++].y;
ctx.moveTo(x+3,y);
ctx.arc(x,y, 3, 0, 2 * Math.PI, false);
}
ctx.fill();
}
// create boxes (box1 is the projected result)
var box = Box();
var box1 = Box();
var box2 = Box();
// create the projection matrix
var iso = Mat();
// angles for X, and Y axis
const xAxis = Math.PI * ( 1 / 4);
const yAxis = Math.PI * ( 4 / 6);
iso.x.set(Math.cos(xAxis), Math.sin(xAxis),0);
iso.y.set(Math.cos(yAxis), Math.sin(yAxis), 0);
// the direction of Z
iso.z.set(0, -1, 0);
// center rendering
ctx.setTransform(1,0,0,1,cw* 0.5,ch);
// transform and render
draw(iso.transform(box,box1));
iso.projectIso(Math.PI * ( 1 / 6), Math.PI * ( 5 / 6), -Math.PI * ( 1 / 2))
ctx.setTransform(1,0,0,1,cw* 1,ch);
draw(iso.transform(box,box1));
iso.rotY(Math.PI / 4);
iso.transform(box,box1);
iso.rotX(Math.atan(1/Math.SQRT2));
iso.transform(box1,box2);
ctx.setTransform(1,0,0,1,cw* 1.5,ch);
draw(box2);
<canvas id="canvas" height='200' width='500'></canvas>
I think the issue may be that the rotation about the x-axis of the room is not 30°. In isometric images there is often an angle of 30° between the sides of a cube and the horizontal. But in order to get this horizontal angle, the rotation around the x-axis should be about 35° (atan(1/sqrt(2))). See the overview in the Wikipedia article.
Having said that, sometimes in computer graphics, the angle between the sides of a cube and the horizontal is about 27° (atan(0.5)), since this produces neater rastered lines on a computer screen. In that case, the rotation around the x-axis is 30°. Check out this article for a lot more information about the different types of projection.

Highcharts connected mappoint with "direction"

I released an interactive map with Highcharts.
It represents the path of an artist in 3 years in Italy.
It contains a serie with 3 points connected.
How can I "print" arrows on the path to esplicate the "direction" of the path?
{
type: "mappoint",
lineWidth: 2,
data: [
{ lat: 41.108679365839755, lon: 16.849069442461108 },
{ lat: 40.65378710700787, lon: 14.759846388659303 },
{ lat: 41.90017321198485, lon: 12.16516614442158 }
]
}
The full code is on jsfiddle
http://jsfiddle.net/0ghkmjpg/
You can wrap a method which is responsible for rendering line in the map and change the path so it shows an arrow between points. You can see the answer how to do it in a simple line chart here.
You can also use Renderer directly and draw a path on load/redraw event.
Function for rendering path might look like this:
function renderArrow(chart, startPoint, stopPoint) {
const triangle = function(x, y, w, h) {
return [
'M', x + w / 2, y,
'L', x + w, y + h,
x, y + h,
'Z'
];
};
var arrow = chart.arrows[startPoint.options.id];
if (!arrow) {
arrow = chart.arrows[startPoint.options.id] = chart.renderer.path().add(startPoint.series.group);
}
const x1 = startPoint.plotX;
const x2 = stopPoint.plotX;
const y1 = startPoint.plotY;
const y2 = stopPoint.plotY;
const distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
const h = Math.min(Math.max(distance / 3, 10), 30);
const w = h / 1.5;
if (distance > h * 2) {
const offset = h / 2 / distance;
const y = y1 + (0.5 + offset) * (y2 - y1);
const x = x1 + (0.5 + offset) * (x2 - x1);
const arrowPath = triangle(x - w / 2, y, w, h);
const angle = Math.round(Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI) + 90;
arrow.attr({
d: arrowPath.join(' '),
transform: `rotate(${angle} ${x} ${y})`,
fill: 'black',
zIndex: 10,
visibility: 'visible'
});
} else {
arrow.attr({
visibility: 'hidden'
});
}
}
And function which loops through the points and render arrows
function renderArrows() {
if (!this.arrows) {
this.arrows = {};
}
const points = this.series[1].points;
points.reduce((start, stop) => {
renderArrow(this, start, stop);
return stop;
});
}
Attach rendering arrows on load/redraw event
Highcharts.mapChart('container', {
chart: {
animation: false,
events: {
load: renderArrows,
redraw: renderArrows
}
},
Of course there is a lot of space in that question how the arrows should behave - should they have always constant size, when should they appear/disappear, exact shape and styles of the arrow, etc. - but you should be able to adjust the code above.
Live example and output
http://jsfiddle.net/jq0oxtpw/

Categories

Resources