Isometric simple sorting with xdim and ydim - javascript

I have a simple isometric sorting system with this function (code is in Typescript/Javascript) :
public Sort(a: PIXI.Sprite, b: PIXI.Sprite) {
return ((a.IsoZ - b.IsoZ) == 0 ? (a.TileZ - b.TileZ == 0 ? (a.Tile2Z ? (a.Tile2Z < b.Tile2Z ? -1 : (a.Tile2Z > b.Tile2Z ? 1 : 0)) : 0) : a.TileZ - b.TileZ) : (a.IsoZ - b.IsoZ));
}
It depends on three parameters:
IsoZ: the first sorting variables, used to sort tiles
TileZ: the tile
sorting variable, used if a.IsoZ == b.IsoZ
Tile2Z: used if a.TileZ == b.TileZ
Here's how IsoZ is basically calculated for most objects:
this.Position is an array of x and y coordinates
this.Position[0] + this.Position[1] + 1000;
now I want to support object x and y dimensions, so how can I implement something like this in this expression?
x and y dimensions values are for example (2, 2) for a cube or (2, 4) for a cuboid
this.Position[0] + this.Position[1] + 1000 // + x dimension + y dimension ???

Isometric visual occlusion sort (depth sort)
Defining depth:
Higher depths values are closer to the screen. Unlike 3D perspective projection where depth is distance from the front plane, this answer uses depth as distance towards the screen.
Iso projection
If you have a iso projection
const P2 = (x = 0,y = 0) => ({x, y});
const isoProjMat = {
xAxis : P2(1 , 0.5),
yAxis : P2(-0.5, 1 ),
zAxis : P2(0 , -1 ),
}
That takes a 3d point and projects to screen space
const P3 = (x = 0, y = 0, z = 0) => ({x, y, z});
isoProjMat.project = function (p, retP = P2()) { // p is 3D point
retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x;
retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y;
return retP;
}
You can add the depth of a point as the z value of the 2D projected point. You need to add a transform axis for the depth.
isoProjMat.depth = P3(0.5,1, 1 );
For x move closer by half its size, y * 1 and z * 1.
The modified project now adds z to the returned point.
isoProjMat.project = function (p, retP = P3()) {
retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x;
retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y;
retP.z = p.x * this.depth.x + p.y * this.depth.y + p.z * this.depth.z;
return retP;
}
Thus for a set of points in 3D space projected to 2D iso screen space you sort on the z
const points = mySetOfPoints(); // what ever your points come from
const projected = points.map(p => isoProjMat.project(p));
projected.sort((a,b) => a.z - b.z);
All good for points but for sprites which occupy a 3D volume this does not work.
What you need to do is add a bounding volume ie a square. If your projection is static then we can simplify the bounding volume to the nearest point. For the box that is the vertex at the top bottom right eg sprite at (0,0,0) has a size (10,10,20) the nearest point in 3d is at (10,10,20).
I can not work your sort out as there is not enough info in the question but I am guessing sprite.Iso is the base origin of the sprite and sprite.Tile & Tile2 represent bounding box.
Thus to get the nearest point
const depthProj = P3(0.5,1, 1 ); // depth projection matrix
// get the depth of each sprite adding the property depth
sprites.forEach(spr => {
const p = {
x : spr.IsoX + Math.max(spr.TileX,spr.Tile2X),
y : spr.IsoY + Math.max(spr.TileY,spr.Tile2Y),
z : spr.IsoZ + Math.max(spr.TileZ,spr.Tile2Z)
};
spr.depth = p.x * depthProj.x + p.y * depthProj.y + p.z * depthProj.z;
})
sprites.sort((a,b) => a.depth - b.depth);
Then render from index 0 up.
An example.
The following is not fully applicable as it sorts by polygons and uses the polygons mean depth rather than its max depth (really should use max but cant be bothered ATM)
I add it only to show how the above code for the isoProjMat is used. It draws stacked boxes from pixel alpha and color rendered on a canvas.
Click rendered result to switch projections from bi-morphic to tri-morphic (as you did not specify the type of projection you used this shows how the depth transform changes between two types of parallel projection.
const ctx = canvas.getContext("2d");
var count = 0;
var firstRun = 0;
function doIt(){
// 3d 2d points
const P3 = (x=0, y=0, z=0) => ({x,y,z});
const P2 = (x=0, y=0) => ({x, y});
// isomorphic projection matrix
const isoProjMat = {
xAxis : count ? P2(1 , 0.5) : P2(1 , 0.5) ,
yAxis : count ? P2(-0.5, 1) : P2(-1 , 0.5) ,
zAxis : count ? P2(0 , -1) : P2(0 , -1) ,
depth : count ? P3(0.5,1, 1) : P3(0.5,0.5,1) , // projections have z as depth
origin : P2(), // (0,0) default 2D point
project (p, retP = P3()) {
retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x + this.origin.x;
retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y + this.origin.y;
retP.z = p.x * this.depth.x + p.y * this.depth.y + p.z * this.depth.z;
return retP;
}
}
// isomorphic mesh shape as vertices and polygons
const isoMesh = (()=>{
const polygon = {
inds : null,
depth : 0,
fillStyle : "#888",
lineWidth : 0.5,
strokeStyle : "#000",
setStyle(ctx) {
ctx.fillStyle = this.fillStyle;
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.strokeStyle;
},
}
const isoShape = {
verts : null,
pVerts : null, // projected verts
polys : null,
addVert(p3 = P3()) { this.verts.push(p3); return p3 },
addPoly(poly = isoShape.createPoly()) { this.polys.push(poly); return poly },
createPoly(options = {}) { return Object.assign({}, polygon, {inds : []}, options) },
render(ctx,mat = isoProjMat) {
var i,j,d;
const pv = this.pVerts === null ? this.pVerts = [] : this.pVerts;
const v = this.verts;
const ps = this.polys;
for(i = 0; i < v.length; i += 1){ pv[i] = mat.project(v[i], pv[i]) }
for(i = 0; i < ps.length; i += 1) {
const p = ps[i];
j = 0; d = 0;
while(j < p.inds.length) { d += pv[p.inds[j++]].z }
p.depth = d / p.inds.length;
}
ps.sort((a,b)=>a.depth - b.depth);
for(i = 0; i < ps.length; i += 1) {
const p = ps[i];
p.setStyle(ctx);
ctx.beginPath();
j = 0;
while(j < p.inds.length) { ctx.lineTo(pv[p.inds[j]].x, pv[p.inds[j++]].y) }
if (p.fillStyle !== "") { ctx.fill() }
if (p.strokeStyle !== "" && p.lineWidth !== 0) {ctx.closePath(); ctx.stroke() }
}
}
}
return () => Object.assign({},isoShape,{verts : [], polys : []});
})();
// Lazy coding I am using Point3 (P3) to hold RGB values
function createBoxMesh(box = isoMesh(), pos = P3(), size = P3(10,10,10), rgb = P3(128,128,128)){ // x,y,z are sizes in those directions
const PA3 = (x,y,z) => P3(x + pos.x, y + pos.y, z + pos.z);
const RGB = (s) => `rgb(${(rgb.x * s) | 0},${(rgb.y * s) | 0},${(rgb.z * s) | 0})`;
const indA = (inds) => inds.map(ind => ind + i);
const i = box.verts.length; // get top vert index
if(typeof size === "number") { size = P3(size,size,size) }
const x = size.x / 2;
const y = size.y / 2;
const z = size.z;
box.addVert(PA3(-x,-y, 0)); // ind 0
box.addVert(PA3( x,-y, 0));
box.addVert(PA3( x, y, 0));
box.addVert(PA3(-x, y, 0));
box.addVert(PA3(-x,-y, z)); // ind 4
box.addVert(PA3( x,-y, z));
box.addVert(PA3( x, y, z));
box.addVert(PA3(-x, y, z));
// box.addPoly(box.createPoly({ inds : indA([0,1,5,4]), fillStyle : RGB(0.5) }));
box.addPoly(box.createPoly({ inds : indA([1,2,6,5]), fillStyle : RGB(0.7) }));
box.addPoly(box.createPoly({ inds : indA([2,3,7,6]), fillStyle : RGB(1) }));
// box.addPoly(box.createPoly({ inds : indA([3,0,4,7]), fillStyle : RGB(0.8) }));
box.addPoly(box.createPoly({ inds : indA([4,5,6,7]), fillStyle : RGB(1.5) }));
return box;
}
function createDrawable(w,h){
const c = document.createElement("canvas");
c.width = w;
c.height = h;
c.ctx = c.getContext("2d");
return c;
}
const map = createDrawable(40,30);
map.ctx.font = "20px arial";
map.ctx.textAlign = "center";
map.ctx.textBaseline = "middle";
map.ctx.fillStyle = "rgba(0,128,0,0.5)";
map.ctx.strokeStyle = "rgba(255,0,0,0.5)";
map.ctx.lineWidth = 2;
map.ctx.fillRect(1,1,map.width - 2, map.height - 2);
map.ctx.strokeRect(1,1,map.width - 2, map.height - 2);
map.ctx.fillStyle = "#AAA";
map.ctx.strokeStyle = "rgba(255,128,0,0.5)";
map.ctx.strokeText("text",map.width / 2, map.height / 2);
map.ctx.fillText("text",map.width / 2, map.height / 2);
var dat = map.ctx.getImageData(0, 0, map.width , map.height).data;
ctx.setTransform(1,0,0,1,0,0);
// get total projection area and size canvas so that the iso projection fits
const boxSize = P3(10,10,5);
const topLeft = isoProjMat.project(P3(0,0,10 * boxSize.z));
const botRight = isoProjMat.project(P3(map.width * boxSize.x,map.height * boxSize.y,0));
const topRight = isoProjMat.project(P3(map.width * boxSize.x,0,0));
const botLeft = isoProjMat.project(P3(0,map.height * boxSize.y,0));
canvas.width = ((topRight.x - botLeft.x) + 10)|0;
canvas.height = ((botRight.y - topLeft.y) + 10)|0;
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.font = "32px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Rendering will take a moment.",Math.min(innerWidth,canvas.width)/2,Math.min(innerHeight,canvas.height)/2)
setTimeout(function(){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.setTransform(1,0,0,1,-botLeft.x+10,-topLeft.y+10);
const alphaThresh = 100;
const boxes = isoMesh();
for(var y = 0; y < map.height; y ++){
for(var x = 0; x < map.width; x ++){
const ind = (x + y * map.width) * 4;
if(dat[ind + 3] > alphaThresh){
const h = (((dat[ind + 3]-alphaThresh)/(255-alphaThresh)) * 10) | 0;
for(var z = 0; z < h; z++){
createBoxMesh(
boxes,
P3(x * boxSize.x,y * boxSize.y, z * boxSize.z),
boxSize,
P3(dat[ind],dat[ind+1],dat[ind+2])
);
}
}
}
}
boxes.render(ctx);
if(firstRun === 0){
firstRun = 1;
ctx.setTransform(1,0,0,1,0,0);
ctx.font = "24px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "black";
ctx.fillText("Bimorphic projection. Click for Trimorphic projection..",canvas.width/2,30)
canvas.onclick =()=>{
count += 1;
count %= 2;
doIt();
};
}
},0);
};
doIt();
canvas {
border : 2px solid black;
}
<canvas id="canvas"></canvas>

Related

Handle mouse hovering image inside of canvas isometric grid

I got a isometric grid in html canvas.
I am trying to handle the mouse hover the buildings.
Some buildings will have different heights.
As you can see in the image below I am hovering a tile, the mouse pointer is inside the blueish tile.
The problem is when the mouse pointer is off the ground tile, or in the middle of the building image, the highlighted tile goes off.
Need a way to click on each individual building, how can this be resolved?
Main basic functions:
let applied_map = ref([]); // tileMap
let tile_images = ref([]); // this will contain loaded IMAGES for canvas to consume from
let tile_height = ref(50);
let tile_width = ref(100);
const renderTiles = (x, y) => {
let tileWidth = tile_width.value;
let tileHeight = tile_height.value;
let tile_half_width = tileWidth / 2;
let tile_half_height = tileHeight / 2;
for (let tileX = 0; tileX < gridSize.value; ++tileX) {
for (let tileY = 0; tileY < gridSize.value; ++tileY) {
let renderX = x + (tileX - tileY) * tile_half_width;
let renderY = y + (tileX + tileY) * tile_half_height;
let tile = applied_map.value[tileY * gridSize.value + tileX];
renderTileBackground(renderX, renderY + 50, tileWidth, tileHeight);
if (tile !== -1) {
if (tile_images.value.length) {
renderTexturedTile(
tile_images.value[tile].img,
renderX,
renderY + 40,
tileHeight
);
}
}
}
}
if (
hoverTileX.value >= 0 &&
hoverTileY.value >= 0 &&
hoverTileX.value < gridSize.value &&
hoverTileY.value < gridSize.value
) {
let renderX = x + (hoverTileX.value - hoverTileY.value) * tile_half_width;
let renderY = y + (hoverTileX.value + hoverTileY.value) * tile_half_height;
renderTileHover(renderX, renderY + 50, tileWidth, tileHeight);
}
};
const renderTileBackground = (x, y, width, height) => {
ctx.value.beginPath();
ctx.value.setLineDash([5, 5]);
ctx.value.strokeStyle = "black";
ctx.value.fillStyle = "rgba(25,34, 44,0.2)";
ctx.value.lineWidth = 1;
ctx.value.moveTo(x, y);
ctx.value.lineTo(x + width / 2, y - height / 2);
ctx.value.lineTo(x + width, y);
ctx.value.lineTo(x + width / 2, y + height / 2);
ctx.value.lineTo(x, y);
ctx.value.stroke();
ctx.value.fill();
};
const renderTexturedTile = (imgSrc, x, y, tileHeight) => {
let offsetY = tileHeight - imgSrc.height;
ctx.value.drawImage(imgSrc, x, y + offsetY);
};
const renderTileHover = (x, y, width, height) => {
ctx.value.beginPath();
ctx.value.setLineDash([]);
ctx.value.strokeStyle = "rgba(161, 153, 255, 0.8)";
ctx.value.fillStyle = "rgba(161, 153, 255, 0.4)";
ctx.value.lineWidth = 2;
ctx.value.moveTo(x, y);
ctx.value.lineTo(x + width / 2, y - height / 2);
ctx.value.lineTo(x + width, y);
ctx.value.lineTo(x + width / 2, y + height / 2);
ctx.value.lineTo(x, y);
ctx.value.stroke();
ctx.value.fill();
};
Updates after answer below
Based on Helder Sepulveda answer I created a function drawCube.
And added to my click function and to the renderTiles. So on click and frame update it creates a cube with 3 faces,and its placed on same position as the building and stores the Path on a global variable, the cube follows the isometric position.
In the drawCube, there is a condition where i need to hide the right face from the cube. Hide if there's a building on the next tile. So if you hover the building it wont trigger the last building on.
//...some code click function
//...
if (tile_images.value[tileIndex] !== undefined) {
drawCube(
hoverTileX.value + tile_height.value,
hoverTileY.value +
Number(tile_images.value[tileIndex].img.height / 2) -
10,
tile_height.value, // grow X pos to left
tile_height.value, // grow X pos to right,
Number(tile_images.value[tileIndex].img.height / 2), // height,
ctx.value,
{
tile_index: tileIndex - 1 < 0 ? 0 : tileIndex - 1,
}
);
}
This is the drawCube
const drawCube = (x, y, wx, wy, h, the_ctx, options = {}) => {
// https://codepen.io/AshKyd/pen/JYXEpL
let path = new Path2D();
let hide_options = {
left_face: false,
right_face: false,
top_face: false,
};
if (options.hasOwnProperty("hide")) {
hide_options = Object.assign(hide_options, options.hide);
}
// left face
if (!hide_options.left_face) {
path.moveTo(x, y);
path.lineTo(x - wx, y - wx * 0.5);
path.lineTo(x - wx, y - h - wx * 0.5);
path.lineTo(x, y - h * 1);
}
// right;
if (
!hide_options.right_face &&
!coliders.value[options.tile_index].hide_right_face
) {
path.moveTo(x, y);
path.lineTo(x + wy, y - wy * 0.5);
path.lineTo(x + wy, y - h - wy * 0.5);
path.lineTo(x, y - h * 1);
}
//top
if (!hide_options.right_face) {
path.moveTo(x, y - h);
path.lineTo(x - wx, y - h - wx * 0.5);
path.lineTo(x - wx + wy, y - h - (wx * 0.5 + wy * 0.5));
path.lineTo(x + wy, y - h - wy * 0.5);
}
// the_ctx.beginPath();
let isONHover = the_ctx.isPointInPath(
path,
mousePosition.x - 10,
mousePosition.y - 10
);
the_ctx.fillStyle = null;
if (isONHover) {
// let indx = options.tile_pos.y * gridSize.value + options.tile_pos.x;
//this is the click on object event
if (isMouseDown.value) {
//Trigger
if (buildozer.value === true) {
coliders.value[options.tile_index] = -1;
applied_map.value[options.tile_index] = -1;
}
isMouseDown.value = false;
}
the_ctx.fillStyle = "green";
}
the_ctx.fill(path);
if (
coliders.value[options.tile_index] == -1 &&
applied_map.value[options.tile_index]
) {
coliders.value[options.tile_index] = path;
}
};
In a nutshell you need to be able to detect mouseover on more complex shapes ...
I recommend you to use Path2d:
https://developer.mozilla.org/en-US/docs/Web/API/Path2D
That way you can build any shape you like and then we have access to isPointInPath to detect if the mouse is over our shape.
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath
Here is a small example:
class Shape {
constructor(x, y, width, height) {
this.path = new Path2D()
this.path.arc(x, y, 12, 0, 2 * Math.PI)
this.path.arc(x, y - 9, 8, 0, 1.5 * Math.PI)
this.path.lineTo(x + width / 2, y)
this.path.lineTo(x, y + height / 2)
this.path.lineTo(x - width / 2, y)
this.path.lineTo(x, y - height / 2)
this.path.lineTo(x + width / 2, y)
}
draw(ctx, pos) {
ctx.beginPath()
ctx.fillStyle = ctx.isPointInPath(this.path, pos.x, pos.y) ? "red" : "green"
ctx.fill(this.path)
}
}
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect()
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
}
}
var canvas = document.getElementById("canvas")
var ctx = canvas.getContext("2d")
shapes = []
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
shapes.push(new Shape(50 + i * 40, 40 + j * 40, 40, 20))
}
}
canvas.addEventListener("mousemove", function(evt) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
var mousePos = getMousePos(canvas, evt)
shapes.forEach((s) => {s.draw(ctx, mousePos)})
},
false
)
shapes.forEach((s) => {
s.draw(ctx, {x: 0, y: 0})
})
<canvas id="canvas" width="200" height="200"></canvas>
This example draws a "complex" shape (two arcs and a few lines) and the shape changes color to red when the mouse is hovering the shape

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

html5 canvas triangle with rounded corners

I'm new to HTML5 Canvas and I'm trying to draw a triangle with rounded corners.
I have tried
ctx.lineJoin = "round";
ctx.lineWidth = 20;
but none of them are working.
Here's my code:
var ctx = document.querySelector("canvas").getContext('2d');
ctx.scale(5, 5);
var x = 18 / 2;
var y = 0;
var triangleWidth = 18;
var triangleHeight = 8;
// how to round this triangle??
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
ctx.closePath();
ctx.fillStyle = "#009688";
ctx.fill();
ctx.fillStyle = "#8BC34A";
ctx.fillRect(0, triangleHeight, 9, 126);
ctx.fillStyle = "#CDDC39";
ctx.fillRect(9, triangleHeight, 9, 126);
<canvas width="800" height="600"></canvas>
Could you help me?
Rounding corners
An invaluable function I use a lot is rounded polygon. It takes a set of 2D points that describe a polygon's vertices and adds arcs to round the corners.
The problem with rounding corners and keeping within the constraint of the polygons area is that you can not always fit a round corner that has a particular radius.
In these cases you can either ignore the corner and leave it as pointy or, you can reduce the rounding radius to fit the corner as best possible.
The following function will resize the corner rounding radius to fit the corner if the corner is too sharp and the lines from the corner not long enough to get the desired radius in.
Note the code has comments that refer to the Maths section below if you want to know what is going on.
roundedPoly(ctx, points, radius)
// ctx is the context to add the path to
// points is a array of points [{x :?, y: ?},...
// radius is the max rounding radius
// this creates a closed polygon.
// To draw you must call between
// ctx.beginPath();
// roundedPoly(ctx, points, radius);
// ctx.stroke();
// ctx.fill();
// as it only adds a path and does not render.
function roundedPoly(ctx, points, radiusAll) {
var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut,radius;
// convert 2 points into vector form, polar form, and normalised
var asVec = function(p, pp, v) {
v.x = pp.x - p.x;
v.y = pp.y - p.y;
v.len = Math.sqrt(v.x * v.x + v.y * v.y);
v.nx = v.x / v.len;
v.ny = v.y / v.len;
v.ang = Math.atan2(v.ny, v.nx);
}
radius = radiusAll;
v1 = {};
v2 = {};
len = points.length;
p1 = points[len - 1];
// for each point
for (i = 0; i < len; i++) {
p2 = points[(i) % len];
p3 = points[(i + 1) % len];
//-----------------------------------------
// Part 1
asVec(p2, p1, v1);
asVec(p2, p3, v2);
sinA = v1.nx * v2.ny - v1.ny * v2.nx;
sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
angle = Math.asin(sinA < -1 ? -1 : sinA > 1 ? 1 : sinA);
//-----------------------------------------
radDirection = 1;
drawDirection = false;
if (sinA90 < 0) {
if (angle < 0) {
angle = Math.PI + angle;
} else {
angle = Math.PI - angle;
radDirection = -1;
drawDirection = true;
}
} else {
if (angle > 0) {
radDirection = -1;
drawDirection = true;
}
}
if(p2.radius !== undefined){
radius = p2.radius;
}else{
radius = radiusAll;
}
//-----------------------------------------
// Part 2
halfAngle = angle / 2;
//-----------------------------------------
//-----------------------------------------
// Part 3
lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
//-----------------------------------------
//-----------------------------------------
// Special part A
if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
lenOut = Math.min(v1.len / 2, v2.len / 2);
cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
} else {
cRadius = radius;
}
//-----------------------------------------
// Part 4
x = p2.x + v2.nx * lenOut;
y = p2.y + v2.ny * lenOut;
//-----------------------------------------
// Part 5
x += -v2.ny * cRadius * radDirection;
y += v2.nx * cRadius * radDirection;
//-----------------------------------------
// Part 6
ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
//-----------------------------------------
p1 = p2;
p2 = p3;
}
ctx.closePath();
}
You may wish to add to each point a radius eg {x :10,y:10,radius:20} this will set the max radius for that point. A radius of zero will be no rounding.
The maths
The following illistration shows one of two possibilities, the angle to fit is less than 90deg, the other case (greater than 90) just has a few minor calculation differences (see code).
The corner is defined by the three points in red A, B, and C. The circle radius is r and we need to find the green points F the circle center and D and E which will define the start and end angles of the arc.
First we find the angle between the lines from B,A and B,C this is done by normalising the vectors for both lines and getting the cross product. (Commented as Part 1) We also find the angle of line BC to the line at 90deg to BA as this will help determine which side of the line to put the circle.
Now we have the angle between the lines, we know that half that angle defines the line that the center of the circle will sit F but we do not know how far that point is from B (Commented as Part 2)
There are two right triangles BDF and BEF which are identical. We have the angle at B and we know that the side DF and EF are equal to the radius of the circle r thus we can solve the triangle to get the distance to F from B
For convenience rather than calculate to F is solve for BD (Commented as Part 3) as I will move along the line BC by that distance (Commented as Part 4) then turn 90deg and move up to F (Commented as Part 5) This in the process gives the point D and moving along the line BA to E
We use points D and E and the circle center F (in their abstract form) to calculate the start and end angles of the arc. (done in the arc function part 6)
The rest of the code is concerned with the directions to move along and away from lines and which direction to sweep the arc.
The code section (special part A) uses the lengths of both lines BA and BC and compares them to the distance from BD if that distance is greater than half the line length we know the arc can not fit. I then solve the triangles to find the radius DF if the line BD is half the length of shortest line of BA and BC
Example use.
The snippet is a simple example of the above function in use. Click to add points to the canvas (needs a min of 3 points to create a polygon). You can drag points and see how the corner radius adapts to sharp corners or short lines. More info when snippet is running. To restart rerun the snippet. (there is a lot of extra code that can be ignored)
The corner radius is set to 30.
const ctx = canvas.getContext("2d");
const mouse = {
x: 0,
y: 0,
button: false,
drag: false,
dragStart: false,
dragEnd: false,
dragStartX: 0,
dragStartY: 0
}
function mouseEvents(e) {
mouse.x = e.pageX;
mouse.y = e.pageY;
const lb = mouse.button;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
if (lb !== mouse.button) {
if (mouse.button) {
mouse.drag = true;
mouse.dragStart = true;
mouse.dragStartX = mouse.x;
mouse.dragStartY = mouse.y;
} else {
mouse.drag = false;
mouse.dragEnd = true;
}
}
}
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
const pointOnLine = {x:0,y:0};
function distFromLines(x,y,minDist){
var index = -1;
const v1 = {};
const v2 = {};
const v3 = {};
const point = P2(x,y);
eachOf(polygon,(p,i)=>{
const p1 = polygon[(i + 1) % polygon.length];
v1.x = p1.x - p.x;
v1.y = p1.y - p.y;
v2.x = point.x - p.x;
v2.y = point.y - p.y;
const u = (v2.x * v1.x + v2.y * v1.y)/(v1.y * v1.y + v1.x * v1.x);
if(u >= 0 && u <= 1){
v3.x = p.x + v1.x * u;
v3.y = p.y + v1.y * u;
dist = Math.hypot(v3.y - point.y, v3.x - point.x);
if(dist < minDist){
minDist = dist;
index = i;
pointOnLine.x = v3.x;
pointOnLine.y = v3.y;
}
}
})
return index;
}
function roundedPoly(ctx, points, radius) {
var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut;
var asVec = function(p, pp, v) {
v.x = pp.x - p.x;
v.y = pp.y - p.y;
v.len = Math.sqrt(v.x * v.x + v.y * v.y);
v.nx = v.x / v.len;
v.ny = v.y / v.len;
v.ang = Math.atan2(v.ny, v.nx);
}
v1 = {};
v2 = {};
len = points.length;
p1 = points[len - 1];
for (i = 0; i < len; i++) {
p2 = points[(i) % len];
p3 = points[(i + 1) % len];
asVec(p2, p1, v1);
asVec(p2, p3, v2);
sinA = v1.nx * v2.ny - v1.ny * v2.nx;
sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
angle = Math.asin(sinA); // warning you should guard by clampling
// to -1 to 1. See function roundedPoly in answer or
// Math.asin(Math.max(-1, Math.min(1, sinA)))
radDirection = 1;
drawDirection = false;
if (sinA90 < 0) {
if (angle < 0) {
angle = Math.PI + angle;
} else {
angle = Math.PI - angle;
radDirection = -1;
drawDirection = true;
}
} else {
if (angle > 0) {
radDirection = -1;
drawDirection = true;
}
}
halfAngle = angle / 2;
lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
lenOut = Math.min(v1.len / 2, v2.len / 2);
cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
} else {
cRadius = radius;
}
x = p2.x + v2.nx * lenOut;
y = p2.y + v2.ny * lenOut;
x += -v2.ny * cRadius * radDirection;
y += v2.nx * cRadius * radDirection;
ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
p1 = p2;
p2 = p3;
}
ctx.closePath();
}
const eachOf = (array, callback) => { var i = 0; while (i < array.length && callback(array[i], i++) !== true); };
const P2 = (x = 0, y = 0) => ({x, y});
const polygon = [];
function findClosestPointIndex(x, y, minDist) {
var index = -1;
eachOf(polygon, (p, i) => {
const dist = Math.hypot(x - p.x, y - p.y);
if (dist < minDist) {
minDist = dist;
index = i;
}
});
return index;
}
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var dragPoint;
var globalTime;
var closestIndex = -1;
var closestLineIndex = -1;
var cursor = "default";
const lineDist = 10;
const pointDist = 20;
var toolTip = "";
// main update function
function update(timer) {
globalTime = timer;
cursor = "crosshair";
toolTip = "";
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if (w !== innerWidth - 4 || h !== innerHeight - 4) {
cw = (w = canvas.width = innerWidth - 4) / 2;
ch = (h = canvas.height = innerHeight - 4) / 2;
} else {
ctx.clearRect(0, 0, w, h);
}
if (mouse.drag) {
if (mouse.dragStart) {
mouse.dragStart = false;
closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
if(closestIndex === -1){
closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
if(closestLineIndex === -1){
polygon.push(dragPoint = P2(mouse.x, mouse.y));
}else{
polygon.splice(closestLineIndex+1,0,dragPoint = P2(mouse.x, mouse.y));
}
}else{
dragPoint = polygon[closestIndex];
}
}
dragPoint.x = mouse.x;
dragPoint.y = mouse.y
cursor = "none";
}else{
closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
if(closestIndex === -1){
closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
if(closestLineIndex > -1){
toolTip = "Click to cut line and/or drag to move.";
}
}else{
toolTip = "Click drag to move point.";
closestLineIndex = -1;
}
}
ctx.lineWidth = 4;
ctx.fillStyle = "#09F";
ctx.strokeStyle = "#000";
ctx.beginPath();
roundedPoly(ctx, polygon, 30);
ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.strokeStyle = "red";
ctx.lineWidth = 0.5;
eachOf(polygon, p => ctx.lineTo(p.x,p.y) );
ctx.closePath();
ctx.stroke();
ctx.strokeStyle = "orange";
ctx.lineWidth = 1;
eachOf(polygon, p => ctx.strokeRect(p.x-2,p.y-2,4,4) );
if(closestIndex > -1){
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
dragPoint = polygon[closestIndex];
ctx.strokeRect(dragPoint.x-4,dragPoint.y-4,8,8);
cursor = "move";
}else if(closestLineIndex > -1){
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
var p = polygon[closestLineIndex];
var p1 = polygon[(closestLineIndex + 1) % polygon.length];
ctx.beginPath();
ctx.lineTo(p.x,p.y);
ctx.lineTo(p1.x,p1.y);
ctx.stroke();
ctx.strokeRect(pointOnLine.x-4,pointOnLine.y-4,8,8);
cursor = "pointer";
}
if(toolTip === "" && polygon.length < 3){
toolTip = "Click to add a corners of a polygon.";
}
canvas.title = toolTip;
canvas.style.cursor = cursor;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas {
border: 2px solid black;
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
I started by using #Blindman67 's answer, which works pretty well for basic static shapes.
I ran into the problem that when using the arc approach, having two points right next to each other is very different than having just one point. With two points next to each other, it won't be rounded, even if that is what your eye would expect. This is extra jarring if you are animating the polygon points.
I fixed this by using Bezier curves instead. IMO this is conceptually a little cleaner as well. I just make each corner with a quadratic curve where the control point is where the original corner was. This way, having two points in the same spot is virtually the same as only having one point.
I haven't compared performance but seems like canvas is pretty good at drawing Beziers.
As with #Blindman67 's answer, this doesn't actually draw anything so you will need to call ctx.beginPath() before and ctx.stroke() after.
/**
* Draws a polygon with rounded corners
* #param {CanvasRenderingContext2D} ctx The canvas context
* #param {Array} points A list of `{x, y}` points
* #radius {number} how much to round the corners
*/
function myRoundPolly(ctx, points, radius) {
const distance = (p1, p2) => Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
const lerp = (a, b, x) => a + (b - a) * x
const lerp2D = (p1, p2, t) => ({
x: lerp(p1.x, p2.x, t),
y: lerp(p1.y, p2.y, t)
})
const numPoints = points.length
let corners = []
for (let i = 0; i < numPoints; i++) {
let lastPoint = points[i]
let thisPoint = points[(i + 1) % numPoints]
let nextPoint = points[(i + 2) % numPoints]
let lastEdgeLength = distance(lastPoint, thisPoint)
let lastOffsetDistance = Math.min(lastEdgeLength / 2, radius)
let start = lerp2D(
thisPoint,
lastPoint,
lastOffsetDistance / lastEdgeLength
)
let nextEdgeLength = distance(nextPoint, thisPoint)
let nextOffsetDistance = Math.min(nextEdgeLength / 2, radius)
let end = lerp2D(
thisPoint,
nextPoint,
nextOffsetDistance / nextEdgeLength
)
corners.push([start, thisPoint, end])
}
ctx.moveTo(corners[0][0].x, corners[0][0].y)
for (let [start, ctrl, end] of corners) {
ctx.lineTo(start.x, start.y)
ctx.quadraticCurveTo(ctrl.x, ctrl.y, end.x, end.y)
}
ctx.closePath()
}
Styles for joining of lines such as ctx.lineJoin="round" apply to the stroke operation on paths - which is when their width, color, pattern, dash/dotted and similar line style attributes are taken into account.
Line styles do not apply to filling the interior of a path.
So to affect line styles a stroke operation is needed. In the following adaptation of posted code, I've translated canvas output to see the result without cropping, and stroked the triangle's path but not the rectangles below it:
var ctx = document.querySelector("canvas").getContext('2d');
ctx.scale(5, 5);
ctx.translate( 18, 12);
var x = 18 / 2;
var y = 0;
var triangleWidth = 48;
var triangleHeight = 8;
// how to round this triangle??
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
ctx.closePath();
ctx.fillStyle = "#009688";
ctx.fill();
// stroke the triangle path.
ctx.lineWidth = 3;
ctx.lineJoin = "round";
ctx.strokeStyle = "orange";
ctx.stroke();
ctx.fillStyle = "#8BC34A";
ctx.fillRect(0, triangleHeight, 9, 126);
ctx.fillStyle = "#CDDC39";
ctx.fillRect(9, triangleHeight, 9, 126);
<canvas width="800" height="600"></canvas>

Javascript find pixels while canvas is rotated

I'm trying to write rotated text (on various angles) on canvas but wish not to overlap the texts. Therefore after rotating the canvas and before filling the text I tried to test the text background using measureText().width and getImageData() to see that there is no text already there to get messed with new. I fail to find the text (coloured pixels) while the canvas is rotated. Here is a simplified version (using rectangle) to my problem. I wonder why no coloured pixels are found?
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas" width="300" height="150" style="border:1px solid black;">
Your browser does not support the HTML5 canvas tag.</canvas>
<script>
var cWidth=300, cHeight= 150;
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
// Rotate context around centre point
ctx.translate( cWidth/2, cHeight/2);
ctx.rotate(20 * Math.PI / 180);
// Draw 100x50px rectangle at centre of rotated ctx
ctx.fillStyle = "yellow";
ctx.fillRect(-50, -25, 100, 50);
// Is my rotated rectangle really there?
// i.e. any coloured pixels at rotated cxt centre
imgData = ctx.getImageData(-50, -25, 100, 50);
// All rectangle pixels should be coloured
for (var j=0; j<imgData.data.length; j++){
if (imgData.data[j] > 0){
alert ("Coloured");
break;
};
};
// Why none is found?
</script>
</body>
</html>
The yellow rectangle should be at the same spot and angle as the tested image data area is. What went wrong? How I can test the colour of a rotated area? Being novice to Javascript I try to avoid libraries at this stage.
pekka
The problem is the translate function. You need to account for the displacement.
Try this:
imgData = ctx.getImageData(-50+cWidth/2,-25+cHeight/2,100,50);
The issue with your code is that getImageData() was targeting the wrong portion of the canvas. The x and y coordinates should have the same value of the translate() function. This is how your code should look like:
// Translate the rectangle, rotate it and fill it
ctx.translate(cWidth/2, cHeight/2);
ctx.rotate(20 * Math.PI / 180);
ctx.fillStyle = "yellow";
ctx.fillRect(-50, -25, 100, 50);
// Get the rectangle rotation
var imgData = ctx.getImageData(cWidth/2, cHeight/2, 100, 50);
And this is the JSfiddle with the complete code. I hope that my answer helps you!
General purpose answer.
This answer will work for any type of transformation and relies on the fact that the 2D context transform is mirrored in javascript. It gets the pixels one by one. Another way is to get the pixels as a block by transforming the corners of the rendered box and finding the bounding box that holds the transformed box. Use that box to get the image data. You will still have to transform each pixel address you want to check as the image data will also include extra pixels outside the rendered box. This may be quicker than the solution presented below if the searched for pixels are far and few between, The solution below is better if the odds of finding the pixel you want are high and you can break out of the iterations early.
You will need to use the transform API at the bottom of this answer
var mMatrix = new Transform(); // creates a default matrix
// define the bounds
const box = {x : -50, y : -25, w : 100, h : 50}; // our box
ctx.translate( cWidth/2, cHeight/2); // set the context transform
ctx.rotate(20 * Math.PI / 180);
// draw the stuff
ctx.fillStyle = "yellow";
ctx.fillRect(box.x, box.y, box.w, box.h);
// mirror the ctx transformations
mMatrix.translate(cWidth/2, cHeight/2).rotate(20 * Math.PI / 180);
var ix,iy,x,y;
var v = new Transform.Vec(); // create a working vec to save memory usage and anyoning GC hits
for(iy = 0; iy < box.h; iy ++){ // for each vertical pixel
for(ix = 0; iy < box.w; ix ++){ // for each horizontal
v.x = ix + 0.5 + box.x; // use pixel center
v.y = iy + 0.5 + box.y;
mMatrix.applyToVec(v); // transform into screen coords.
v.x = Math.floor(v.x); // get rid of grogens
v.y = Math.floor(v.y);
// now we have the pixel address corresponding to the box coordinate ix,iy
// get one pixel first check if it is on the canvas
if(v.x >= 0 && v.x < ctx.canvas.width && v.y >= 0 && v.y < ctx.canvas.height){
var pD = ctx.getImageData(v.x,v.y,1,1).data;
var red = pD[0];
var green = pD[1];
var blue = pD[2];
var alpha = pD[3];
// now you have the RGB values
... do what ever you want with that info
}
}
}
Transform API
This is the transform API required by the answer. It is a cut down of a API written by me and you can do with what you want (apart from evil things). See answer for usage. It is just the very basics, you can find more comprehensive Transform API on the net (but I don't think you will find one much faster)
See comment at the bottom for method details. Most functions are chainable.
Code snippet runs nothing.
var Transform = (function () {
var tx, ty, v1, v2, v3, mat;
// work vecs and transform provide pre assigned working memory
v1 = new Vec();
v2 = new Vec();
v3 = new Vec();
mat = new Transform();
ty = tx = 0;
function Transform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) {
if (xAxisX === undefined) { // create identity matrix
this.xAxis = new Vec(); // Default vec is 1,0
this.yAxis = new Vec(0, 1);
this.origin = new Vec(0, 0);
} else if (yAxisY === undefined) { // if only 3 arguments assume that the 3 arguments are vecs
this.xAxis = new Vec(xAxisX.x, xAxisX.y); //
this.yAxis = new Vec(xAxisY.x, xAxisY.y);
this.origin = new Vec(yAxisY.x, xAxisY.y);
} else {
this.xAxis = new Vec(xAxisX, xAxisY); // Default vec is 1,0
this.yAxis = new Vec(yAxisX, yAxisY);
this.origin = new Vec(originX, originY);
}
};
function Vec(x, y) {
if (x === undefined || x === null) {
this.x = 1;
this.y = 0;
} else {
this.x = x;
this.y = y;
}
};
Vec.prototype = {
copy : function () {
return new Vec(this.x, this.y);
},
setAs : function (vec, y) { // set this to the value of vec, or if two arguments vec is x and y is y
if (y !== undefined) {
this.x = vec;
this.y = y;
return this;
}
this.x = vec.x;
this.y = vec.y;
return this;
}
}
Transform.prototype = {
xAxis : undefined,
yAxis : undefined,
origin : undefined,
Vec : Vec, // expose the Vec interface
copy : function () {
return new Transform(this.xAxis, this.yAxis, this.origin);
},
setAs : function (transform) {
this.xAxis.x = transform.xAxis.x;
this.xAxis.y = transform.xAxis.y;
this.yAxis.x = transform.yAxis.x;
this.yAxis.y = transform.yAxis.y;
this.origin.x = transform.origin.x;
this.origin.y = transform.origin.y;
return;
},
reset : function () { // resets this to the identity transform
this.xAxis.x = 1;
this.xAxis.y = 0;
this.yAxis.x = 0;
this.yAxis.y = 1;
this.origin.x = 0;
this.origin.y = 0;
return this;
},
apply : function (x, y) { // returns an object {x : trabsformedX, y : trabsformedY} the returned object does not have the Vec prototype
return {
x : x * this.xAxis.x + y * this.yAxis.x + this.origin.x,
y : x * this.xAxis.y + y * this.yAxis.y + this.origin.y
};
},
applyToVec : function (vec) { // WARNING returns this not the vec.
tx = vec.x * this.xAxis.x + vec.y * this.yAxis.x + this.origin.x;
vec.y = vec.x * this.xAxis.y + vec.y * this.yAxis.y + this.origin.y;
vec.x = tx;
return this;
},
invert : function () { // inverts the transform
// first check if just a scale translated identity matrix and invert that as it is quicker
if (this.xAxis.y === 0 && this.yAxis.x === 0 && this.xAxis.x !== 0 && this.yAxis.y !== 0) {
this.xAxis.x = 1 / this.xAxis.x;
this.xAxis.y = 0;
this.yAxis.x = 0;
this.yAxis.y = 1 / this.yAxis.y;
this.origin.x = -this.xAxis.x * this.origin.x;
this.origin.y = -this.yAxis.y * this.origin.y;
return this;
}
var cross = this.xAxis.x * this.yAxis.y - this.xAxis.y * this.yAxis.x;
v1.x = this.yAxis.y / cross;
v1.y = -this.xAxis.y / cross;
v2.x = -this.yAxis.x / cross;
v2.y = this.xAxis.x / cross;
v3.x = (this.yAxis.x * this.origin.y - this.yAxis.y * this.origin.x) / cross;
v3.y = - (this.xAxis.x * this.origin.y - this.xAxis.y * this.origin.x) / cross;
this.xAxis.x = v1.x;
this.xAxis.y = v1.y;
this.yAxis.x = v2.x;
this.yAxis.y = v2.y;
this.origin.x = v3.x;
this.origin.y = v3.y;
return this;
},
asInverse : function (transform) { // creates a new or uses supplied transform to return the inverse of this matrix
if (transform === undefined) {
transform = new Transform();
}
if (this.xAxis.y === 0 && this.yAxis.x === 0 && this.xAxis.x !== 0 && this.yAxis.y !== 0) {
transform.xAxis.x = 1 / this.xAxis.x;
transform.xAxis.y = 0;
transform.yAxis.x = 0;
transform.yAxis.y = 1 / this.yAxis.y;
transform.origin.x = -transform.xAxis.x * this.origin.x;
transform.origin.y = -transform.yAxis.y * this.origin.y;
return transform;
}
var cross = this.xAxis.x * this.yAxis.y - this.xAxis.y * this.yAxis.x;
transform.xAxis.x = this.yAxis.y / cross;
transform.xAxis.y = -this.xAxis.y / cross;
transform.yAxis.x = -this.yAxis.x / cross;
transform.yAxis.y = this.xAxis.x / cross;
transform.origin.x = (this.yAxis.x * this.origin.y - this.yAxis.y * this.origin.x) / cross;
transform.origin.y = - (this.xAxis.x * this.origin.y - this.xAxis.y * this.origin.x) / cross;
return transform;
},
multiply : function (transform) { // multiplies this with transform
var tt = transform;
var t = this;
v1.x = tt.xAxis.x * t.xAxis.x + tt.yAxis.x * t.xAxis.y;
v1.y = tt.xAxis.y * t.xAxis.x + tt.yAxis.y * t.xAxis.y;
v2.x = tt.xAxis.x * t.yAxis.x + tt.yAxis.x * t.yAxis.y;
v2.y = tt.xAxis.y * t.yAxis.x + tt.yAxis.y * t.yAxis.y;
v3.x = tt.xAxis.x * t.origin.x + tt.yAxis.x * t.origin.y + tt.origin.x;
v3.y = tt.xAxis.y * t.origin.x + tt.yAxis.y * t.origin.y + tt.origin.y;
t.xAxis.x = v1.x;
t.xAxis.y = v1.y;
t.yAxis.x = v2.x;
t.yAxis.y = v2.y;
t.origin.x = v3.x;
t.origin.y = v3.y;
return this;
},
rotate : function (angle) { // Multiply matrix by rotation matrix at angle
var xdx = Math.cos(angle);
var xdy = Math.sin(angle);
v1.x = xdx * this.xAxis.x + (-xdy) * this.xAxis.y;
v1.y = xdy * this.xAxis.x + xdx * this.xAxis.y;
v2.x = xdx * this.yAxis.x + (-xdy) * this.yAxis.y;
v2.y = xdy * this.yAxis.x + xdx * this.yAxis.y;
v3.x = xdx * this.origin.x + (-xdy) * this.origin.y;
v3.y = xdy * this.origin.x + xdx * this.origin.y;
this.xAxis.x = v1.x;
this.xAxis.y = v1.y;
this.yAxis.x = v2.x;
this.yAxis.y = v2.y;
this.origin.x = v3.x;
this.origin.y = v3.y;
return this;
},
scale : function (scaleX, scaleY) { // Multiply the matrix by scaleX and scaleY
this.xAxis.x *= scaleX;
this.xAxis.y *= scaleY;
this.yAxis.x *= scaleX;
this.yAxis.y *= scaleY;
this.origin.x *= scaleX;
this.origin.y *= scaleY;
return this;
},
translate : function (x, y) { // Multiply the matrix by translate Matrix
this.origin.x += x;
this.origin.y += y;
return this;
},
setTransform : function (xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) {
this.xAxis.x = xAxisX;
this.xAxis.y = xAxisY;
this.yAxis.x = yAxisX;
this.yAxis.y = yAxisY;
this.origin.x = originX;
this.origin.y = originY;
return this;
},
transform : function (xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) {
var t = this;
v1.x = xAxisX * t.xAxis.x + yAxisX * t.xAxis.x;
v1.y = xAxisY * t.xAxis.x + yAxisY * t.xAxis.y;
v2.x = xAxisX * t.yAxis.x + yAxisX * t.yAxis.x;
v2.y = xAxisY * t.yAxis.x + yAxisY * t.yAxis.y;
v3.x = xAxisX * t.origin.x + yAxisX * t.origin.y + originX;
v3.y = xAxisY * t.origin.x + yAxisY * t.origin.y + originY;
t.xAxis.x = v1.x;
t.xAxis.y = v1.y;
t.yAxis.x = v2.x;
t.yAxis.y = v2.y;
t.origin.x = v3.x;
t.origin.y = v3.y;
return this;
},
contextTransform : function (ctx) {
ctx.transform(this.xAxis.x, this.xAxis.y, this.yAxis.x, this.yAxis.y, this.origin.x, this.origin.y);
return this;
},
contextSetTransform : function (ctx) {
ctx.Settransform(this.xAxis.x, this.xAxis.y, this.yAxis.x, this.yAxis.y, this.origin.x, this.origin.y);
return this;
},
setFromCurrentContext : function(ctx){
if(ctx && typeof ctx.currentTransform === "object"){
var mat = ctx.currentTransform;
this.xAxis.x = mat.a;
this.xAxis.y = mat.b;
this.yAxis.x = mat.c;
this.yAxis.y = mat.d;
this.origin.x = mat.e;
this.origin.y = mat.f;
}
return this;
}
}
if(typeof document.createElement("canvas").getContext("2d").currentTransform !== "object"){
Transform.prototype.setFromCurrentContext = undefined;
}
return Transform;
})();
/*
rotate(angle) // Multiply matrix by rotation matrix at angle. Same as ctx.rotate
scale(scaleX, scaleY) // Multiply the matrix by scaleX and scaleY. Same as ctx.scale
translate(x, y) // Multiply the matrix by translate Matrix. Same as ctx.translate
setTransform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) //Replaces the current reansform with the new values. Same as ctx.setTransform
transform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) // multiplies this transform with the supplied transform. Same as ctx.transform
Transform.xAxis // Vec object defining the direction and scale of the x Axis. Values are in canvas pixel coordinates
Transform.yAxis // Vec object defining the direction and scale of the y Axis. Values are in canvas pixel coordinates
Transform.origin // Vec object defining canvas pixel coordinates of the origin
Transform.Vec // interface to a vec object with basic interface needed to support Transform
Transform.reset() // resets the transform to the identity matrix (the default matrix used by 2D context)
Transform.copy() // creates a new copy of this object
Transform.setAs(transform) // sets the content of this to the values of the argument transform
Transform.apply(x, y) { // Transforms the coords x,y by multiplying them with this. Returns an object {x : trabsformedX, y : trabsformedY} the returned object does not have the Vec prototype
Transform.applyToVec(vec) // transforms the point vec. WARNING returns this not the vec.
Transform.invert() // inverts the transform
Transform.asInverse(transform) // creates a new or uses supplied transform to return the inverse of this matrix
Transform.multiply(transform) // multiplies this with transform
Transform.contextTransform(ctx) // multiplies the supplied context (ctx) transform by this.
Transform.contextSetTransform(ctx) // set the supplied context (ctx) transform to this
Transform.setFromCurrentContext(ctx) // Only for supported browser. Sets this to the supplied context current transformation. May not be available if there is no browser support
There is also access to the very simple Vec object. To create a vec `new Transform.Vec(x,y)`
*/

HTML5 Canvas - Drawing Linear Gradients on a Circle (Color Wheel)

I'm trying to draw a circle with, not radial gradients, but linear gradients that go around the circle... Basically, I'm trying to create a color wheel and it has to be dynamic as the colors will be customizable... However, I'm completely baffled on how to approach this matter...
I thought I could draw my own circle and color it, then loop the process with a larger radius to fill it out. But that proved to not only be extremely ineffecient but very buggy too...
Here was my first attempt: http://jsfiddle.net/gyFqX/1/
I stuck with that method but changed it to fill a 2x2 square for each point on the circle. It worked alright for blending up to 3 colors, but then you begin to notice it's distortion.
Anyway, I've continued working on it a bit and this is what I have now: http://jsfiddle.net/f3SQ2/
var ctx = $('#canvas')[0].getContext('2d'),
points = [],
thickness = 80;
for( var n = 0; n < thickness; n++ )
rasterCircle( 200, 200, (50 + n) );
function fillPixels() {
var size = points.length,
colors = [
hexToRgb( '#ff0000' ), // Red
hexToRgb( '#ff00ff' ), // Magenta
hexToRgb( '#0000ff' ), // Blue
hexToRgb( '#00ffff' ), // Teal
hexToRgb( '#00ff00' ), // Green
hexToRgb( '#ffff00' ), // Yellow
hexToRgb( '#ff0000' ), // Red
],
colorSpan = colors.length - 1;
if ( colors.length > 0 ) {
var lastPadding = size % colorSpan,
stepSize = size / colorSpan,
steps = null,
cursor = 0;
for ( var index = 0; index < colorSpan; index++ ) {
steps = Math.floor( ( index == colorSpan - 1 ) ? stepSize + lastPadding : stepSize );
createGradient( colors[ index ], colors[ index + 1 ], steps, cursor );
cursor += steps;
}
}
function createGradient( start, end, steps, cursor ) {
for ( var i = 0; i < steps; i++ ) {
var r = Math.floor( start.r + ( i * ( end.r - start.r ) / steps ) ),
g = Math.floor( start.g + ( i * ( end.g - start.g ) / steps ) ),
b = Math.floor( start.b + ( i * ( end.b - start.b ) / steps ) );
ctx.fillStyle = "rgba("+r+","+g+","+b+",1)";
ctx.fillRect( points[cursor][0], points[cursor][1], 2, 2 );
cursor++;
}
}
points = [];
}
function setPixel( x, y ) {
points.push( [ x, y ] );
}
function rasterCircle(x0, y0, radius) {
var f = 1 - radius,
ddF_x = 1,
ddF_y = -2 * radius,
x = 0,
y = radius;
setPixel(x0, y0 + radius);
while(x < y) {
if(f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
setPixel(x0 - x, y0 - y);
}
var temp = [];
f = 1 - radius,
ddF_x = 1,
ddF_y = -2 * radius,
x = 0,
y = radius;
while(x < y) {
if(f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
temp.push( [x0 - y, y0 - x] );
}
temp.push( [x0 - radius, y0] );
for(var i = temp.length - 1; i > 0; i--)
setPixel( temp[i][0], temp[i][1] );
fillPixels();
}
What I'm trying to accomplish is something like this: http://img252.imageshack.us/img252/3826/spectrum.jpg
The 'brightness' (white to black fade) is not an issue as I know it can be accomplished by using a radial gradient after the color spectrum has been drawn. However, I'd appreciate some help in figuring out how to draw the spectrum itself.
I was even thinking I could draw a linear one and then bend (transform) it, but there aren't any native functions to do that and tackling something such as that is above my skill level. :-/
Check this out: http://jsfiddle.net/f3SQ2/5/
var can = $('#canvas')[0],
ctx = can.getContext('2d'),
radius = 120,
thickness = 80,
p = {
x: can.width,
y: can.height
},
start = Math.PI,
end = start + Math.PI / 2,
step = Math.PI / 180,
ang = 0,
grad,
r = 0,
g = 0,
b = 0,
pct = 0;
ctx.translate(p.x, p.y);
for (ang = start; ang <= end; ang += step) {
ctx.save();
ctx.rotate(-ang);
// linear gradient: black->current color->white
grad = ctx.createLinearGradient(0, radius - thickness, 0, radius);
grad.addColorStop(0, 'black');
h = 360-(ang-start)/(end-start) * 360;
s = '100%';
l = '50%';
grad.addColorStop(.5, 'hsl('+[h,s,l].join()+')');
grad.addColorStop(1, 'white');
ctx.fillStyle = grad;
// the width of three for the rect prevents gaps in the arc
ctx.fillRect(0, radius - thickness, 3, thickness);
ctx.restore();
}
Edit: fixed color spectrum. Apparently we can just give it HSL values, no need for conversions or messy calculations!
Modified a few things to handle scaling better: http://jsfiddle.net/f3SQ2/6/
step = Math.PI / 360
ctx.fillRect(0, radius - thickness, radius/10, thickness);
You could for example set the gradient stops like so:
h = 360-(ang-start)/(end-start) * 360;
s = '100%';
grad.addColorStop(0, 'hsl('+[h,s,'0%'].join()+')'); //black
grad.addColorStop(.5,'hsl('+[h,s,'50%'].join()+')'); //color
grad.addColorStop(1, 'hsl('+[h,s,'100%'].join()+')');//white
My first note would be that the image you linked to has all 3 components it doesn't need to change and could just be a static image.
I adapted some code from a project i'm working on:
http://jsfiddle.net/f3SQ2/1/
function drawColourArc(image) {
var data = image.data;
var i = 0;
var w = image.width, h = image.height;
var result = [0, 0, 0, 1];
var outer = 1, inner = 0.5;
var mid = 0.75;
for (var y = 0; y < h; y++) {
for (var x = 0; x < w; x++) {
var dx = (x / w) - 1, dy = (y / w) - 1;
var angular = ((Math.atan2(dy, dx) + Math.PI) / (2 * Math.PI)) * 4;
var radius = Math.sqrt((dx * dx) + (dy * dy));
if (radius < inner || radius > outer) {
data[i++] = 255;
data[i++] = 255;
data[i++] = 255;
data[i++] = 0;
}
else {
if (radius < mid) {
var saturation = 1;
var brightness = (radius - 0.5) * 4;
}
else {
var saturation = 1- ((radius - 0.75) * 4);
var brightness = 1;
}
result[0] = angular;
result[1] = saturation;
result[2] = brightness;
result[3] = 1;
//Inline HSBToRGB
if (result[1] == 0) {
result[0] = result[1] = result[2] = result[2];
}
else {
var varH = result[0] * 6;
var varI = Math.floor(varH); //Or ... var_i = floor( var_h )
var var1 = result[2] * (1 - result[1]);
var var2 = result[2] * (1 - result[1] * (varH - varI));
var var3 = result[2] * (1 - result[1] * (1 - (varH - varI)));
if (varI == 0 || varI == 6) {
result[0] = result[2];
result[1] = var3;
result[2] = var1;
}
else if (varI == 1) {
result[0] = var2;
result[1] = result[2];
result[2] = var1;
}
else if (varI == 2) {
result[0] = var1;
result[1] = result[2];
result[2] = var3;
}
else if (varI == 3) {
result[0] = var1;
result[1] = var2;
result[2] = result[2];
}
else if (varI == 4) {
result[0] = var3;
result[1] = var1;
result[2] = result[2];
}
else {
result[0] = result[2];
result[1] = var1;
result[2] = var2;
}
}
//End of inline
data[i++] = result[0] * 255;
data[i++] = result[1] * 255;
data[i++] = result[2] * 255;
data[i++] = result[3] * 255;
}
}
}
};
var canvas = document.getElementsByTagName("canvas")[0];
var ctx = canvas.getContext("2d");
var image = ctx.createImageData(canvas.width, canvas.height);
drawColourArc(image);
ctx.putImageData(image, 0, 0);
This does it per-pixel which is accurate but you may want to draw an outline to combat the aliasing. It could be adapted to use custom colours instead of interpolating hue.

Categories

Resources