Drawing scalloped polygon between multiple points - javascript

I am trying to draw a scalloped path using SVG between multiple points like it is drawn for rectangle here but between multiple points. Expecting two or more two or more selected points to be connected by scalloped line.
But the problems I am facing are,
Scallops are not symmetric or random in sizes. - I solved this
After clicking multiple points scallops directions and up down. Like in below image.
I am completely ok even if the answer is given in html5 canvas context. I will make adjustments. I am missing some extra calculation but could not figure out what.
Please click multiple times in result page to see the scallops drawn currently
var strokeWidth = 3;
function distance(x1, y1, x2, y2) {
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
function findNewPoint(x, y, angle, distance) {
var result = {};
result.x = Math.round(Math.cos(angle) * distance + x);
result.y = Math.round(Math.sin(angle) * distance + y);
return result;
}
function getAngle(x1, y1, x2, y2) {
return Math.atan2(y2 - y1, x2 - x1);
}
function scapolledLine(points, strokeWidth) {
var that = this;
var scallopSize = strokeWidth * 8;
var path = [],
newP = null;
path.push("M", points[0].x, points[0].y);
points.forEach(function(s, i) {
var stepW = scallopSize,
lsw = 0;
var e = points[i + 1];
if (!e) {
path.push('A', stepW / 2, stepW / 2, "0 0 1", s.x, s.y);
return;
}
var args = [s.x, s.y, e.x, e.y];
var dist = that.distance.apply(that, args);
if (dist === 0) return;
var angle = that.getAngle.apply(that, args);
newP = s;
// Number of possible scallops between current points
var n = dist / stepW,
crumb;
if (dist < (stepW * 2)) {
stepW = (dist - stepW) > (stepW * 0.38) ? (dist / 2) : dist;
} else {
n = (n - (n % 1));
crumb = dist - (n * stepW);
/*if((stepW - crumb) > (stepW * 0.7)) {
lsw = crumb;
} else {
stepW += (crumb / n);
}*/
stepW += (crumb / n);
}
// Recalculate possible scallops.
n = dist / stepW;
var aw = stepW / 2;
for (var i = 0; i < n; i++) {
newP = that.findNewPoint(newP.x, newP.y, angle, stepW);
if (i === (n - 1)) {
aw = (lsw > 0 ? lsw : stepW) / 2;
}
path.push('A', aw, aw, "0 0 1", newP.x, newP.y);
}
// scallopSize = stepW;
});
return path.join(' ');
// return path.join(' ') + (points.length > 3 ? 'z' : '');
}
var points = [];
var mouse = null;
var dblclick = null,
doneEnding = false;
window.test.setAttribute('stroke-width', strokeWidth);
function feed() {
if (dblclick && doneEnding) return;
if (!dblclick && (points.length > 0 && mouse)) {
var arr = points.slice(0);
arr.push(mouse);
var str = scapolledLine(arr, strokeWidth);
window.test.setAttribute('d', str);
} else if (dblclick) {
points.push(points[0]);
doneEnding = true;
var str = scapolledLine(points, strokeWidth);
window.test.setAttribute('d', str);
}
}
document.addEventListener('mousedown', function(event) {
points.push({
x: event.clientX,
y: event.clientY
});
feed();
});
document.addEventListener('dblclick', function(event) {
dblclick = true;
feed();
});
document.addEventListener('mousemove', function(event) {
if (points.length > 0) {
mouse = {
x: event.clientX,
y: event.clientY
}
feed();
}
});
body,
html {
height: 100%;
width: 100%;
margin: 0;
padding: 0
}
svg {
height: 100%;
width: 100%
}
<svg id="svgP">
<path id="test" style="stroke: RGBA(212, 50, 105, 1.00); fill: none" />
</svg>

Finding circle to fit 3Points
This method uses a function that finds a circle that fits 3 points. Two of the points are the set of points you have. The 3rd point is taken perpendicular to the line between the points and moved out by a factor of the line seg length.
When the circle is found then the start and end angle from the circle center point is found to make the arc segment and that is all done. Just draw the arcs with ctx.arc(
I am not sure exactly what you want. I have it so the arcs all bend up, But it is easy to make the go around.
If you want them all the same size you have to separate the points be equal distance, which is very simple, but means its hard to fit a given area.
Demo
The demo lets you add and drag point. Mouse wheel changes arc depth.
The const at the top arcDepth determines how deep each arc is compared to line segment length. It is a fraction.
You can make it a constant in pixels see, calcArc for how to change.
Each arc has an independent depth so if you don't like the overlapping arcs reduce the depth for that arc (in the code of course).
Hope that helps.
const pointSize = 4;
const pointCol = "#4AF";
var arcDepth = -0.5; // depth of arc as a factor of line seg length
// Note to have arc go the other (positive) way you have
// to change the ctx.arc draw call by adding anticlockwise flag
// see drawArc for more
const arcCol = "#4FA";
const arcWidth = 3;
// Find a circle that fits 3 points.
function fitCircleTo3P(p1x, p1y, p2x, p2y, p3x, p3y, arc) {
var vx,
vy,
c,
c1,
u;
c = (p2x - p1x) / (p1y - p2y); // slope of vector from vec 1 to vec 2
c1 = (p3x - p2x) / (p2y - p3y); // slope of vector from vec 2 to vec 3
// This will not happen in this example
if (c === c1) { // if slope is the same they must be on the same line
return null; // points are in a line
}
// locate the center
if (p1y === p2y) { // special case with p1 and p2 have same y
vx = (p1x + p2x) / 2;
vy = c1 * vx + (((p2y + p3y) / 2) - c1 * ((p2x + p3x) / 2));
} else
if (p2y === p3y) { // special case with p2 and p3 have same y
vx = (p2x + p3x) / 2;
vy = c * vx + (((p1y + p2y) / 2) - c * ((p1x + p2x) / 2));
} else {
vx = ((((p2y + p3y) / 2) - c1 * ((p2x + p3x) / 2)) - (u = ((p1y + p2y) / 2) - c * ((p1x + p2x) / 2))) / (c - c1);
vy = c * vx + u;
}
arc.x = vx;
arc.y = vy;
vx = p1x - vx;
vy = p1y - vy;
arc.rad = Math.sqrt(vx * vx + vy * vy);
return arc;
}
var points = [];
var arcs = [];
function addArc(p1, p2, depth) {
// remove next 5 line if you dont want all arcs to face the same way.
if(points[p1][0] > points[p2][0]){
var temp = p1;
p1 = p2;
p2 = temp;
}
var arc = {
p1 : p1,
p2 : p2,
depth : depth,
rad : null, // radius
a1 : null, // angle from
a2 : null, // angle to
x : null,
y : null,
}
arcs.push(arc);
return arc;
}
function calcArc(arc, depth) {
var p = points[arc.p1]; // get points
var pp = points[arc.p2];
// change depth if needed
depth = arc.depth = depth !== undefined ? depth : arc.depth;
var vx = pp[0] - p[0]; // vector from p to pp
var vy = pp[1] - p[1];
var cx = (pp[0] + p[0]) / 2; // center point
var cy = (pp[1] + p[1]) / 2; // center point
var len = Math.sqrt(vx * vx + vy * vy); // get length
cx -= vy * depth; // find 3 point at 90 deg to line and dist depth
cy += vx * depth;
// To have depth as a fixed length uncomment 4 lines below and comment out 2 lines above.
//var nx = vx / len; // normalise vector
//var ny = vy / len;
//cx -= ny * depth; // find 3 point at 90 deg to line and dist depth
//cy += nx * depth;
fitCircleTo3P(p[0], p[1], cx, cy, pp[0], pp[1], arc); // get the circle that fits
arc.a1 = Math.atan2(p[1] - arc.y, p[0] - arc.x); // get angle from circle center to first point
arc.a2 = Math.atan2(pp[1] - arc.y, pp[0] - arc.x); // get angle from circle center to second point
}
function addPoint(x, y) {
points.push([x, y]);
}
function drawPoint(x, y, size, col) {
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
}
function drawArc(arc, width, col) {
ctx.lineCap = "round";
ctx.strokeStyle = col;
ctx.lineWidth = width;
ctx.beginPath();
ctx.arc(arc.x, arc.y, arc.rad, arc.a1, arc.a2,false); // true for anti clock wise
ctx.stroke();
}
function findClosestPoint(x, y, dist) {
var index = -1;
for (var i = 0; i < points.length; i++) {
var p = points[i];
var vx = x - p[0];
var vy = y - p[1];
var d = Math.sqrt(vx * vx + vy * vy);
if (d < dist) {
dist = d;
index = i;
}
}
return index;
}
var dragging = false;
var drag = -1;
var dragX, dragY;
var recalcArcs = false;
function display() {
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
if(mouse.w > 0){
arcDepth *= 1.05;
mouse.w = 0;
recalcArcs = true;
}
if(mouse.w < 0){
arcDepth *= 1/1.05;
mouse.w = 0;
recalcArcs = true;
}
if (mouse.buttonRaw & 1) {
if (!dragging) {
var i = findClosestPoint(mouse.x, mouse.y, pointSize * 3);
if (i > -1) {
drag = i;
dragging = true;
dragX = mouse.x - points[drag][0];
dragY = mouse.y - points[drag][1];
}
}
if (dragging) {
points[drag][0] = mouse.x - dragX
points[drag][1] = mouse.y - dragY
recalcArcs = true;
} else {
addPoint(mouse.x, mouse.y);
if (points.length > 1) {
calcArc(addArc(points.length - 2, points.length - 1, arcDepth));
}
mouse.buttonRaw = 0;
}
} else {
if (dragging) {
dragging = false;
drag = -1;
recalcArcs = true;
}
var i = findClosestPoint(mouse.x, mouse.y, pointSize * 3);
if (i > -1) {
canvas.style.cursor = "move";
} else {
canvas.style.cursor = "default";
}
}
for (var i = 0; i < arcs.length; i++) {
if (recalcArcs) {
calcArc(arcs[i],arcDepth);
}
drawArc(arcs[i], arcWidth, arcCol);
}
recalcArcs = false;
for (var i = 0; i < points.length; i++) {
var p = points[i];
drawPoint(p[0], p[1], pointSize, pointCol);
}
}
//===========================================================================================
// END OF ANSWER
// Boiler plate code from here down. Does mouse,canvas,resize and what not
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true; ;
(function () {
const RESIZE_DEBOUNCE_TIME = 100;
var createCanvas,
resizeCanvas,
setGlobals,
resizeCount = 0;
createCanvas = function () {
var c,
cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if (firstRun) {
onResize();
firstRun = false;
} else {
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
w : 0,
alt : false,
shift : false,
ctrl : false,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
crashRecover : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left;
m.y = e.pageY - m.bounds.top;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
} else if (t === "mousewheel") {
m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") {
m.w = -e.detail;
}
e.preventDefault();
}
m.start = function (element) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
m.element.removeEventListener("contextmenu", preventDefault);
m.element = m.callbacks = undefined;
m.active = false;
}
}
return mouse;
})();
function update(timer) { // Main update loop
if (ctx === undefined) {
return;
}
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
setTimeout(function () {
resizeCanvas();
mouse.start(canvas, true);
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
}, 0);
})();
Left click to add point. Left click drag to move points.<br>
Mouse wheel changes arc depth.
Take two...
Maybe this is what you are after.. Sorry its a bit of a mess as I am short on time at the moment.
Same code as before just add the points to the outside of the box, making sure that the width and height steps are equally spaced from the edge.
const pointSize = 4;
const pointCol = "#4AF";
var arcDepth = -0.5; // depth of arc as a factor of line seg length
// Note to have arc go the other (positive) way you have
// to change the ctx.arc draw call by adding anticlockwise flag
// see drawArc for more
const arcCol = "#F92";
const arcWidth = 8;
// Find a circle that fits 3 points.
function fitCircleTo3P(p1x, p1y, p2x, p2y, p3x, p3y, arc) {
var vx,
vy,
c,
c1,
u;
c = (p2x - p1x) / (p1y - p2y); // slope of vector from vec 1 to vec 2
c1 = (p3x - p2x) / (p2y - p3y); // slope of vector from vec 2 to vec 3
// This will not happen in this example
if (c === c1) { // if slope is the same they must be on the same line
return null; // points are in a line
}
// locate the center
if (p1y === p2y) { // special case with p1 and p2 have same y
vx = (p1x + p2x) / 2;
vy = c1 * vx + (((p2y + p3y) / 2) - c1 * ((p2x + p3x) / 2));
} else
if (p2y === p3y) { // special case with p2 and p3 have same y
vx = (p2x + p3x) / 2;
vy = c * vx + (((p1y + p2y) / 2) - c * ((p1x + p2x) / 2));
} else {
vx = ((((p2y + p3y) / 2) - c1 * ((p2x + p3x) / 2)) - (u = ((p1y + p2y) / 2) - c * ((p1x + p2x) / 2))) / (c - c1);
vy = c * vx + u;
}
arc.x = vx;
arc.y = vy;
vx = p1x - vx;
vy = p1y - vy;
arc.rad = Math.sqrt(vx * vx + vy * vy);
return arc;
}
var points = [];
var arcs = [];
function addArc(p1, p2, depth) {
var arc = {
p1 : p1,
p2 : p2,
depth : depth,
rad : null, // radius
a1 : null, // angle from
a2 : null, // angle to
x : null,
y : null,
}
arcs.push(arc);
return arc;
}
function calcArc(arc, depth) {
var p = points[arc.p1]; // get points
var pp = points[arc.p2];
// change depth if needed
depth = arc.depth = depth !== undefined ? depth : arc.depth;
var vx = pp[0] - p[0]; // vector from p to pp
var vy = pp[1] - p[1];
var cx = (pp[0] + p[0]) / 2; // center point
var cy = (pp[1] + p[1]) / 2; // center point
var len = Math.sqrt(vx * vx + vy * vy); // get length
cx -= vy * depth; // find 3 point at 90 deg to line and dist depth
cy += vx * depth;
// To have depth as a fixed length uncomment 4 lines below and comment out 2 lines above.
//var nx = vx / len; // normalise vector
//var ny = vy / len;
//cx -= ny * depth; // find 3 point at 90 deg to line and dist depth
//cy += nx * depth;
fitCircleTo3P(p[0], p[1], cx, cy, pp[0], pp[1], arc); // get the circle that fits
arc.a1 = Math.atan2(p[1] - arc.y, p[0] - arc.x); // get angle from circle center to first point
arc.a2 = Math.atan2(pp[1] - arc.y, pp[0] - arc.x); // get angle from circle center to second point
}
function addPoint(x, y) {
points.push([x, y]);
}
function drawPoint(x, y, size, col) {
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
}
function drawArcStart(width,col){
ctx.lineCap = "round";
ctx.strokeStyle = col;
ctx.lineJoin = "round";
ctx.lineWidth = width;
ctx.beginPath();
}
function drawArc(arc){
ctx.arc(arc.x,arc.y,arc.rad,arc.a1,arc.a2);
}
function drawArcDone(){
ctx.closePath();
ctx.stroke();
}
function findClosestPoint(x, y, dist) {
var index = -1;
for (var i = 0; i < points.length; i++) {
var p = points[i];
var vx = x - p[0];
var vy = y - p[1];
var d = Math.sqrt(vx * vx + vy * vy);
if (d < dist) {
dist = d;
index = i;
}
}
return index;
}
var dragging = false;
var drag = -1;
var dragX, dragY;
var recalcArcs = false;
var box;
//========================================================================
// New box code from her down
// creates the box when canvas is ready
var onResize = function(){
box = {
x : canvas.width * (1/8),
y : canvas.height * (1/8),
w : canvas.width * (6/8),
h : canvas.height * (6/8),
recalculate : true,
arcCount : 20, // number of arcs to try and fit. Does not mean that it will happen
}
}
function display() {
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
if(mouse.w !== 0){
if(mouse.buttonRaw & 4){ // change arc depth
if(mouse.w < 0){
arcDepth *= 1/1.05;
}else{
arcDepth *= 1.05;
}
recalcArcs = true;
}else{ // change arc count
box.arcCount += Math.sign(mouse.w);
box.arcCount = Math.max(4,box.arcCount);
box.recalculate = true;
}
mouse.w = 0;
}
// drag out box;
if(mouse.buttonRaw & 1){
if(!dragging){
box.x = mouse.x;
box.y = mouse.y;
dragging = true;
}
box.w = mouse.x - box.x;
box.h = mouse.y - box.y;
box.recalculate = true;
if(box.w <0){
box.x = box.x + box.w;
box.w = - box.w;
}
if(box.h <0){
box.y = box.y + box.h;
box.h = - box.h;
}
}else{
dragging = false;
}
// stop error
if(box.w === 0 || box.h === 0){
box.recaculate = false;
}
// caculate box arcs
if(box.recalculate){
// reset arrays
points.length = 0;
arcs.length = 0;
// get perimiter length
var perimLen = (box.w + box.h)* 2;
// get estimated step size
var step = perimLen / box.arcCount;
// get inset size for width and hight
var wInStep = (box.w - (Math.floor(box.w/step)-1)*step) / 2;
var hInStep = (box.h - (Math.floor(box.h/step)-1)*step) / 2;
// fix if box to narrow
if(box.w < step){
wInStep = 0;
hInStep = 0;
step = box.h / (Math.floor(box.h/step));
}else if(box.h < step){
wInStep = 0;
hInStep = 0;
step = box.w / (Math.floor(box.w/step));
}
// Add points clock wise
var x = box.x + wInStep;
while(x < box.x + box.w){ // across top
addPoint(x,box.y);
x += step;
}
var y = box.y + hInStep;
while(y < box.y + box.h){ // down right side
addPoint(box.x + box.w,y);
y += step;
}
x = box.x + box.w - wInStep;
while(x > box.x){ // left along bottom
addPoint(x,box.y + box.h);
x -= step;
}
var y = box.y + box.h - hInStep;
while(y > box.y){ // up along left side
addPoint(box.x,y);
y -= step;
}
// caculate arcs.
for(var i =0; i <points.length; i++){
calcArc(addArc(i,(i + 1) % points.length,arcDepth));
}
box.recalculate = false;
}
// recaculate arcs if needed
for(var i = 0; i < arcs.length; i ++){
if(recalcArcs){
calcArc(arcs[i],arcDepth);
}
}
// draw arcs
drawArcStart(arcWidth,arcCol)
for(var i = 0; i < arcs.length; i ++){
drawArc(arcs[i]);
}
drawArcDone();
recalcArcs = false;
}
//===========================================================================================
// END OF ANSWER
// Boiler plate code from here down. Does mouse,canvas,resize and what not
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true; ;
(function () {
const RESIZE_DEBOUNCE_TIME = 100;
var createCanvas,
resizeCanvas,
setGlobals,
resizeCount = 0;
createCanvas = function () {
var c,
cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if (firstRun) {
onResize();
firstRun = false;
} else {
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
w : 0,
alt : false,
shift : false,
ctrl : false,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
crashRecover : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left;
m.y = e.pageY - m.bounds.top;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
} else if (t === "mousewheel") {
m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") {
m.w = -e.detail;
}
e.preventDefault();
}
m.start = function (element) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
m.element.removeEventListener("contextmenu", preventDefault);
m.element = m.callbacks = undefined;
m.active = false;
}
}
return mouse;
})();
function update(timer) { // Main update loop
if (ctx === undefined) {
return;
}
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
setTimeout(function () {
resizeCanvas();
mouse.start(canvas, true);
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
}, 0);
})();
Left click drag to create a box<br>Mouse wheel to change arc count<br>Hold right button down and wheel to change arc depth.<br>

The following code snippet determines the direction (CW, CCW) of each segment by analyzing the adjacent segments. For segment A, if both adjacent segments are on the same side of A (or if A has only one adjacent segment), there is no ambiguity and the scallops of segment A are on the outside of the convex shape formed by these segments. However, if the adjacent segments are on opposite sides of A, in a zigzag pattern, the adjacent segment that extends the farthest from segment A is chosen to set the direction of segment A.
function distance(x1, y1, x2, y2) {
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
function findNewPoint(x, y, angle, distance) {
var result = {};
result.x = Math.round(Math.cos(angle) * distance + x);
result.y = Math.round(Math.sin(angle) * distance + y);
return result;
}
function getAngle(x1, y1, x2, y2) {
return Math.atan2(y2 - y1, x2 - x1);
}
function getSeparationFromLine(lineOrigin, lineAngle, pt) {
x = pt.x - lineOrigin.x;
y = pt.y - lineOrigin.y;
return -x * Math.sin(lineAngle) + y * Math.cos(lineAngle);
}
function getDirection(pts, index, closed) {
var last = pts.length - 1;
var start = index;
var end = (closed && start == last) ? 0 : index + 1;
var prev = (closed && start == 0) ? last : start - 1;
var next = (closed && end == last) ? 0 : end + 1;
var isValidSegment = 0 <= start && start <= last && 0 <= end && end <= last && end !== start;
if (!isValidSegment) {
return 1;
}
var pt1 = pts[start];
var pt2 = pts[end];
var pt, x, y;
var ccw = 0.0;
var theta = Math.atan2(pt2.y - pt1.y, pt2.x - pt1.x);
if (0 <= prev && prev <= last) {
ccw += getSeparationFromLine(pt1, theta, pts[prev]);
}
if (0 <= next && next <= last) {
ccw += getSeparationFromLine(pt1, theta, pts[next]);
}
return ccw > 0 ? "1" : "0";
}
function scapolledLine(pts, closed, strokeWidth) {
var that = this;
var scallopSize = strokeWidth * 8;
var lastIndex = pts.length - 1;
var path = [], newP = null;
path.push("M", pts[0].x, pts[0].y);
pts.forEach(function (s, currentIndex) {
var stepW = scallopSize, lsw = 0;
var isClosingSegment = closed && currentIndex == lastIndex;
var nextIndex = isClosingSegment ? 0 : currentIndex + 1;
var e = pts[nextIndex];
if (!e) {
return;
}
var direction = getDirection(pts, currentIndex, closed);
var args = [s.x, s.y, e.x, e.y];
var dist = that.distance.apply(that, args);
if (dist === 0) {
return;
}
var angle = that.getAngle.apply(that, args);
newP = s;
// Number of possible scallops between current pts
var n = dist / stepW, crumb;
if (dist < (stepW * 2)) {
stepW = (dist - stepW) > (stepW * 0.38) ? (dist / 2) : dist;
} else {
n = (n - (n % 1));
crumb = dist - (n * stepW);
stepW += (crumb / n);
}
// Recalculate possible scallops.
n = dist / stepW;
var aw = stepW / 2;
for (var i = 0; i < n; i++) {
newP = that.findNewPoint(newP.x, newP.y, angle, stepW);
if (i === (n - 1)) {
aw = (lsw > 0 ? lsw : stepW) / 2;
}
path.push('A', aw, aw, "0 0 " + direction, newP.x, newP.y);
}
if (isClosingSegment) {
path.push('A', stepW / 2, stepW / 2, "0 0 " + direction, e.x, e.y);
}
});
return path.join(' ');
}
var strokeWidth = 3;
var points = [];
var mouse = null;
var isClosed = false;
window.test.setAttribute('stroke-width', strokeWidth);
function feed(isDoubleClick) {
if (isClosed) {
return;
}
if (!isDoubleClick && (points.length > 0 && mouse)) {
var arr = points.slice(0);
arr.push(mouse);
var str = scapolledLine(arr, isClosed, strokeWidth);
window.test.setAttribute('d', str);
} else if (isDoubleClick) {
isClosed = true;
points.pop();
var str = scapolledLine(points, isClosed, strokeWidth);
window.test.setAttribute('d', str);
}
}
document.addEventListener('mousedown', function (event) {
points.push({
x: event.clientX,
y: event.clientY
});
feed(false);
});
document.addEventListener('dblclick', function (event) {
feed(true);
});
document.addEventListener('mousemove', function (event) {
if (points.length > 0) {
mouse = {
x: event.clientX,
y: event.clientY
}
feed(false);
}
});
body,
html {
height: 100%;
width: 100%;
margin: 0;
padding: 0
}
svg {
height: 100%;
width: 100%
}
<svg id="svgP">
<path id="test" style="stroke: RGBA(212, 50, 105, 1.00); fill: none" />
</svg>

Related

arrow with modification point

Hi I'm trying to reproduce a draw.io effect. When you draw an arrow it display a blue point in the middle of the arrow that allows you to create angle between the two lines and it display two blue point that allow you to do the same with the two new line. I have added image below. It'll be easier to understand.
I wonder how to code dynamically these blue points that allow to "break" the line
var ctx = tempcanvas.getContext('2d'),
mainctx = canvas.getContext('2d'),
w = canvas.width,
h = canvas.height,
x1,
y1,
isDown = false;
ctx.translate(0.5, 0.5);
tempcanvas.onmousedown = function(e) {
var rect = canvas.getBoundingClientRect();
x1 = e.clientX - rect.left;
y1 = e.clientY - rect.top;
isDown = true;
}
tempcanvas.onmouseup = function() {
isDown = false;
mainctx.drawImage(tempcanvas, 0, 0);
ctx.clearRect(0, 0, w, h);
}
tempcanvas.onmousemove = function(e) {
if (!isDown) return;
var rect = canvas.getBoundingClientRect(),
x2 = e.clientX - rect.left,
y2 = e.clientY - rect.top;
var p0={x1,y1};
var p1={x2,y2};
ctx.clearRect(0, 0, w, h);
drawLineWithArrowhead(p0,p1,25);
}
function drawLineWithArrowhead(p0,p1,headLength){
var PI=Math.PI;
var degreesInRadians225=225*PI/180;
var degreesInRadians135=135*PI/180;
var dx=p1.x2-p0.x1;
var dy=p1.y2-p0.y1;
var angle=Math.atan2(dy,dx);
// calc arrowhead points
var x225=p1.x2+headLength*Math.cos(angle+degreesInRadians225);
var y225=p1.y2+headLength*Math.sin(angle+degreesInRadians225);
var x135=p1.x2+headLength*Math.cos(angle+degreesInRadians135);
var y135=p1.y2+headLength*Math.sin(angle+degreesInRadians135);
ctx.beginPath();
// draw the line from p0 to p1
ctx.moveTo(p0.x1,p0.y1);
ctx.lineTo(p1.x2,p1.y2);
// draw partial arrowhead at 225 degrees
ctx.moveTo(p1.x2,p1.y2);
ctx.lineTo(x225,y225);
// draw partial arrowhead at 135 degrees
ctx.moveTo(p1.x1,p1.y1);
ctx.lineTo(x135,y135);
// stroke the line and arrowhead
ctx.stroke();
}
canvas {position:absolute;left:0;top:0}
#canvas {background:#eef}
<canvas id="canvas" width=400 height=400></canvas>
<canvas id="tempcanvas" width=400 height=400></canvas>
Example snippet
Sorry out of time (Weekend and all) to write a detailed explanation and no point wasting the code, so hope it helps.
const ctx = canvas.getContext("2d");
ctx.bounds = canvas.getBoundingClientRect();
const P2 = (x = 0, y = 0) => ({x, y});
const points = [];
const lineStyle = "#000";
const nearLineStyle = "#0AF";
const lineWidth = 2;
const nearLineWidth = 3;
const pointStyle = "#000";
const nearPointStyle = "#0AF";
const pointLineWidth = 1;
const nearPointLineWidth = 2;
const arrowSize = 18;
const pointSize = 5;
const nearPointSize = 15;
const checkerSize = 256; // power of two
const checkerCol1 = "#CCC";
const checkerCol2 = "#EEE";
const MIN_SELECT_DIST = 20; // in pixels;
var w = canvas.width, h = canvas.height;
var cw = w / 2, ch = h / 2;
var cursor = "default";
var toolTip = "";
const mouse = { x: 0, y: 0, button: 0 };
const drag = {dragging: false};
requestAnimationFrame(update);
function mouseEvents(e) {
mouse.x = e.pageX - ctx.bounds.left - scrollX;
mouse.y = e.pageY - ctx.bounds.top - scrollY;
if (e.type === "mousedown") { mouse.button |= 1 << (e.which - 1) }
else if (e.type === "mouseup") { mouse.button &= ~(1 << (e.which - 1)) }
}
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
const checkerboard = (()=> {
const s = checkerSize, s2 = s / 2;
const c = document.createElement("canvas");
c.height = c.width = checkerSize;
const ctx = c.getContext("2d", {alpha: false});
ctx.fillStyle = checkerCol1;
ctx.fillRect(0,0,s, s);
ctx.fillStyle = checkerCol2;
ctx.fillRect(0,0,s2,s2);
ctx.fillRect(s2,s2,s2,s2);
ctx.globalAlpha = 0.25;
var ss = s2;
while(ss > 8) {
ctx.fillStyle = ctx.createPattern(c, "repeat");
ctx.setTransform(1/8,0,0,1/8,0,0);
ctx.fillRect(0,0,s * 8,s * 8);
ss /= 2;
}
return ctx.createPattern(c, "repeat");
})();
function nearestPointLine(points, point, minDist){ // fills nearest object with nearest point and line to point if within minDist.
var i = 0, p1, dist;
nearest.reset(minDist);
const v1 = P2();
const v2 = P2();
const v3 = P2();
for (const p of points) {
v2.x = point.x - p.x;
v2.y = point.y - p.y;
dist = (v2.x * v2.x + v2.y * v2.y) ** 0.5;
if(dist < nearest.point.dist) {
nearest.point.dist = dist;
nearest.point.p = p;
nearest.point.idx = i;
}
if (p1) {
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) { // is closest poin on line segment
v3.x = p.x + v1.x * u;
v3.y = p.y + v1.y * u;
//ctx.fillRect(v3.x, v3.y, 5, 5)
dist = ((v3.y - point.y) ** 2 + (v3.x - point.x) ** 2) ** 0.5;
if(dist < nearest.line.dist) {
nearest.line.dist = dist;
nearest.line.p1 = p1;
nearest.line.p2 = p;
nearest.line.idx = i;
nearest.line.onLine.x = v3.x;
nearest.line.onLine.y = v3.y;
}
}
}
p1 = p;
i ++;
}
if (nearest.point.idx > -1 && nearest.point.dist / 2 <= nearest.line.dist) {
nearest.active = nearest.point;
nearest.near = true;
} else if (nearest.line.idx > -1) {
nearest.active = nearest.line;
nearest.near = true;
}
}
function drawLine(p1, p2) {
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
}
function drawLineArrow(p1, p2) {
var nx = p1.x - p2.x;
var ny = p1.y - p2.y;
const d =( nx * nx + ny * ny) ** 0.5;
if(d > arrowSize) {
nx /= d;
ny /= d;
ctx.setTransform(-nx, -ny, ny, -nx, p2.x, p2.y);
ctx.beginPath()
ctx.fillStyle = ctx.strokeStyle;
ctx.moveTo(0, 0);
ctx.lineTo(-arrowSize, arrowSize / 2);
ctx.lineTo(-arrowSize, -arrowSize / 2);
ctx.fill();
ctx.setTransform(1,0,0,1,0,0);
}
}
function drawPoint(p, size = pointSize) {
ctx.rect(p.x - size / 2, p.y - size / 2, size, size);
}
function drawLines(points) {
var p1;
ctx.strokeStyle = lineStyle;
ctx.lineWidth = lineWidth;
ctx.beginPath()
for(const p of points) {
if (p1) { drawLine(p1 ,p) }
p1 = p;
}
ctx.stroke();
if(points.length > 1) {
drawLineArrow(points[points.length - 2], p1);
}
}
function drawPoints(points) {
ctx.strokeStyle = pointStyle;
ctx.lineWidth = pointLineWidth;
ctx.beginPath()
for(const p of points) { drawPoint(p) }
ctx.stroke();
}
function sizeCanvas() {
if (w !== innerWidth || h !== innerHeight) {
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
ctx.bounds = canvas.getBoundingClientRect();
}
}
const nearest = {
point: { isPoint: true },
line: { onLine: P2() },
reset(minDist) {
nearest.point.dist = minDist;
nearest.point.idx = -1;
nearest.line.dist = minDist;
nearest.line.idx = -1;
nearest.active = null;
nearest.near = false;
},
draw() {
const a = nearest.active;
if (a) {
if (a.isPoint) {
ctx.strokeStyle = nearPointStyle;
ctx.lineWidth = nearPointLineWidth;
ctx.beginPath()
drawPoint(a.p, nearPointSize);
ctx.stroke();
} else {
ctx.strokeStyle = nearLineStyle;
ctx.lineWidth = nearLineWidth;
ctx.beginPath()
drawLine(a.p1, a.p2);
ctx.stroke();
ctx.strokeStyle = nearPointStyle;
ctx.lineWidth = nearPointLineWidth;
ctx.beginPath()
drawPoint(a.onLine, nearPointSize);
ctx.stroke();
}
}
}
}
function update() {
cursor = "crosshair";
toolTip = "";
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
sizeCanvas();
ctx.fillStyle = checkerboard;
ctx.fillRect(0, 0, w, h);
if (!drag.dragging) {
nearestPointLine(points, mouse, MIN_SELECT_DIST);
if (nearest.near && nearest.active.isPoint) { cursor = "move"; toolTip = "Drag to move point"}
else if (nearest.near) { cursor = "crosshair"; toolTip = "Click/drag to cut and drag new point" }
else {
if (points.length < 2) {
cursor = "crosshair";
toolTip ="Click to add point";
} else {
cursor = "default";
toolTip = "";
}
}
}
drawLines(points);
drawPoints(points);
nearest.draw();
if((mouse.button & 1) === 1) {
if (!drag.dragging) {
if(points.length < 2 && !nearest.near) {
points.push(P2(mouse.x, mouse.y));
mouse.button = 0;
} else if (nearest.near) {
if (nearest.active.isPoint) {
drag.point = nearest.active.p;
} else {
drag.point = P2(nearest.active.onLine.x, nearest.active.onLine.y);
points.splice(nearest.active.idx, 0, drag.point);
nearestPointLine(points, drag.point, 20);
}
drag.offX = drag.point.x - mouse.x;
drag.offY = drag.point.y - mouse.y;
drag.dragging = true;
}
}
if(drag.dragging) {
drag.point.x = drag.offX + mouse.x;
drag.point.y = drag.offY + mouse.y;
drag.point.x = drag.point.x < 1 ? 1 : drag.point.x > w - 2 ? w - 2 : drag.point.x;
drag.point.y = drag.point.y < 1 ? 1 : drag.point.y > h - 2 ? h - 2 : drag.point.y;
cursor = "none";
}
} else if((mouse.button & 1) === 0) {
drag.dragging = false;
drag.point = null;
}
canvas.title = toolTip;
canvas.style.cursor = cursor;
requestAnimationFrame(update);
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>

How to color line in canvas?

EDIT: I'm trying to get a single line colored, not the whole grid
So, what I'm trying to do is that when the canvas gets clicked, the lines selected move and get colored.
I already have the lines moving but I cannot get them colored. I know I have to use the strokeStyle property, but how do I get the specific line colored?
I tried putting the strokeStyle under the draw_each method, but that doesn't work, and I don't know why. Here's my code:
//draw each line in array
function draw_each(p1, p2, p3, p4) {
$.moveTo(p1.x, p1.y);
$.lineTo(p2.x, p2.y);
$.moveTo(p1.x, p1.y);
$.lineTo(p4.x, p4.y);
if (p1.ind_x == gnum - 2) {
$.moveTo(p3.x, p3.y);
$.lineTo(p4.x, p4.y);
}
if (p1.ind_y == gnum - 2) {
$.moveTo(p3.x, p3.y);
$.lineTo(p2.x, p2.y);
}
}
https://codepen.io/diazabdulm/pen/qJyrZP?editors=0010
It seems to me that what you want is actually to generate a color per segment based on the force that's been applied to it, so that the wave effect keeps its effect.
What you can do, is to save the median of each node's velocity in both axis as a mean to represent the force they're currently experiencing, and in your drawing part to get the median of all nodes that will compose each segment of your grid and compose a color from it.
But this comes with a drawback: your code was well written enough to compose the whole grid a single sub-path, thus limiting the number of drawings necessary. But now, we have to make each segment its own sub-path (since they'll got their own color), so we'll loose a lot in terms of performances...
var gnum = 90; //num grids / frame
var _x = 2265; //x width (canvas width)
var _y = 1465; //y height (canvas height)
var w = _x / gnum; //grid sq width
var h = _y / gnum; //grid sq height
var $; //context
var parts; //particles
var frm = 0; //value from
var P1 = 0.0005; //point one
var P2 = 0.01; //point two
var n = 0.98; //n value for later
var n_vel = 0.03; //velocity
var ŭ = 0; //color update
var msX = 0; //mouse x
var msY = 0; //mouse y
var msdn = false; //mouse down flag
var Part = function() {
this.x = 0; //x pos
this.y = 0; //y pos
this.vx = 0; //velocity x
this.vy = 0; //velocity y
this.ind_x = 0; //index x
this.ind_y = 0; //index y
};
Part.prototype.frame = function() {
if (this.ind_x == 0 || this.ind_x == gnum - 1 || this.ind_y == 0 || this.ind_y == gnum - 1) {
return;
}
var ax = 0; //angle x
var ay = 0; //angle y
//off_dx, off_dy = offset distance x, y
var off_dx = this.ind_x * w - this.x;
var off_dy = this.ind_y * h - this.y;
ax = P1 * off_dx;
ay = P1 * off_dy;
ax -= P2 * (this.x - parts[this.ind_x - 1][this.ind_y].x);
ay -= P2 * (this.y - parts[this.ind_x - 1][this.ind_y].y);
ax -= P2 * (this.x - parts[this.ind_x + 1][this.ind_y].x);
ay -= P2 * (this.y - parts[this.ind_x + 1][this.ind_y].y);
ax -= P2 * (this.x - parts[this.ind_x][this.ind_y - 1].x);
ay -= P2 * (this.y - parts[this.ind_x][this.ind_y - 1].y);
ax -= P2 * (this.x - parts[this.ind_x][this.ind_y + 1].x);
ay -= P2 * (this.y - parts[this.ind_x][this.ind_y + 1].y);
this.vx += (ax - this.vx * n_vel);
this.vy += (ay - this.vy * n_vel);
//EDIT\\
// store the current velocity (here base on 100 since it will be used with hsl())
this.color = (Math.abs(this.vx)+Math.abs(this.vy)) * 50;
this.x += this.vx * n;
this.y += this.vy * n;
if (msdn) {
var dx = this.x - msX;
var dy = this.y - msY;
var ɋ = Math.sqrt(dx * dx + dy * dy);
if (ɋ > 50) {
ɋ = ɋ < 10 ? 10 : ɋ;
this.x -= dx / ɋ * 5;
this.y -= dy / ɋ * 5;
}
}
};
function go() {
parts = []; //particle array
for (var i = 0; i < gnum; i++) {
parts.push([]);
for (var j = 0; j < gnum; j++) {
var p = new Part();
p.ind_x = i;
p.ind_y = j;
p.x = i * w;
p.y = j * h;
parts[i][j] = p;
}
}
}
//move particles function
function mv_part() {
for (var i = 0; i < gnum; i++) {
for (var j = 0; j < gnum; j++) {
var p = parts[i][j];
p.frame();
}
}
}
//draw grid function
function draw() {
//EDIT
// we unfortunately have to break the drawing part
// since each segment has its own color, we can't have a single sub-path anymore...
ŭ -= .5;
for (var i = 0; i < gnum - 1; i += 1) {
for (var j = 0; j < gnum - 1; j += 1) {
var p1 = parts[i][j];
var p2 = parts[i][j + 1];
var p3 = parts[i + 1][j + 1];
var p4 = parts[i + 1][j];
draw_each(p1, p2, p3, p4);
}
}
}
//draw each in array
function draw_each(p1, p2, p3, p4) {
// for each segment we set the color
$.strokeStyle = `hsl(0deg, ${(p1.color+p2.color+p3.color+p4.color) / 4}%, 50%)`;
// begin a new sub-path
$.beginPath();
$.moveTo(p1.x, p1.y);
$.lineTo(p2.x, p2.y);
$.moveTo(p1.x, p1.y);
$.lineTo(p4.x, p4.y);
if (p1.ind_x == gnum - 2) {
$.moveTo(p3.x, p3.y);
$.lineTo(p4.x, p4.y);
}
if (p1.ind_y == gnum - 2) {
$.moveTo(p3.x, p3.y);
$.lineTo(p2.x, p2.y);
}
// and stroke it
$.stroke();
}
//call functions to run
function calls() {
$.fillStyle = "hsla(0, 0%, 7%, 1)";
$.fillRect(0, 0, _x, _y);
mv_part();
draw();
frm++;
}
var c = document.getElementById('canv');
var $ = c.getContext('2d');
$.fillStyle = "hsla(0, 0%, 7%, 1)";
$.fillRect(0, 0, _x, _y);
function resize() {
if (c.width < window.innerWidth) {
c.width = window.innerWidth;
}
if (c.height < window.innerHeight) {
c.height = window.innerHeight;
}
}
requestAnimationFrame(go);
document.addEventListener('click', MSMV, false);
document.addEventListener('click', MSDN, false);
function MSDN(e) {
msdn = true;
window.setTimeout(function() {
msdn = false;
}, 100);
}
function MSUP(e) {
msdn = false;
}
function MSMV(e) {
var rect = e.target.getBoundingClientRect();
msX = e.clientX - rect.left;
msY = e.clientY - rect.top;
}
window.onload = function() {
run();
function run() {
requestAnimationFrame(calls);
requestAnimationFrame(run);
}
resize();
};
onresize = resize;
body {
width: 100%;
overflow: hidden;
cursor:move;
}
<canvas id="canv" width="150" height="150"></canvas>
Now, a more performant way, but less good looking, would be to draw a radial-gradient where the click happened, with a bit of compositing, you'd be able to get something cheap that might work, but it would be quite a lot of coding actually to get multiple such gradients at the same time...
That works for me:
//draw each line in array
function draw_each(p1, p2, p3, p4) {
$.moveTo(p1.x, p1.y);
$.lineTo(p2.x, p2.y);
$.moveTo(p1.x, p1.y);
$.lineTo(p4.x, p4.y);
if (p1.ind_x == gnum - 2) {
$.moveTo(p3.x, p3.y);
$.lineTo(p4.x, p4.y);
}
if (p1.ind_y == gnum - 2) {
$.moveTo(p3.x, p3.y);
$.lineTo(p2.x, p2.y);
}
$.strokeStyle = '#458920';
}

how to fill particles inside rectangle?

I'm looking to draw a rectangle basically text but just for clearing insight I'm working it with rectangle with small particles inside rectangle the basic I idea I got from https://yalantis.com/ but in my attempt I'm stuck here with solid filled rectangle with a color I have specified for particles. Please help me.. :)
Thanks here is my code:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Off Screen Canvas</title>
<script>
function createOffscreenCanvas() {
var offScreenCanvas = document.createElement('canvas');
offScreenCanvas.width = '1360';
offScreenCanvas.height = '400';
var context = offScreenCanvas.getContext("2d");
var W=200;
var H=200;
particleCount = 200;
particles = []; //this is an array which will hold our particles Object/Class
function Particle() {
this.x = Math.random() * W;
this.y = Math.random() * H;
this.direction ={"x": -1 + Math.random()*2, "y": -1 + Math.random()*2};
this.vx = 2 * Math.random() + 4 ;
this.vy = 2 * Math.random() + 4;
this.radius = .9 * Math.random() + 1;
this.move = function(){
this.x += this.vx * this.direction.x;
this.y += this.vy * this.direction.y;
};
this.changeDirection = function(axis){
this.direction[axis] *= -1;
};
this.draw = function() {
context.beginPath();
context.fillStyle = "#0097a7";
context.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
context.fill();
};
this.boundaryCheck = function(){
if(this.x >= W){
this.x = W;
this.changeDirection("x");
}
else if(this.x <= 0){
this.x = 0;
this.changeDirection("x");
}
if(this.y >= H){
this.y = H;
this.changeDirection("y");
}
else if(this.y <= 0){
this.y = 0;
this.changeDirection("y");
}
};
}
function createParticles(){
for (var i = particleCount-1; i >= 0; i--) {
p = new Particle();
particles.push(p);
}
}// end createParticles
function drawParticles(){
for (var i = particleCount-1; i >= 0; i--){
p = particles[i];
p.draw();
}
} //end drawParticles
function updateParticles(){
for(var i = particles.length - 1; i >=0; i--){
p = particles[i];
p.move();
p.boundaryCheck();
}
}//end updateParticle
createParticles();
var part=drawParticles();
context.fillStyle=part;
context.fillRect(W-190, H-190, W, H);
context.fill();
return offScreenCanvas;
}
function copyToOnScreen(offScreenCanvas) {
var onScreenContext=document.getElementById('onScreen').getContext('2d');
var offScreenContext = offScreenCanvas.getContext('2d');
var image=offScreenContext.getImageData(10,10,200,200);
onScreenContext.putImageData(image,offScreenCanvas.width/2,offScreenCanvas.height/4);
}
function main() {
copyToOnScreen(createOffscreenCanvas());
}
</script>
<style>
canvas {
border: 1px solid red;
}
</style>
</head>
<body onload="main()">
<canvas id="onScreen" width="1360" height="400"></canvas>
</body>
</html>
I see you have not found what you are looking for yet. Below is something quick to get you on your way. There is a whole range of stuff being used from canvas,mouse,particles, etc most of which is without comments. There is no load balancing or compliance testing and because it uses babel to be compatible with IE11 I have no clue how it runs on those browsers.
I will add to this answer some other time but for now I am a little over it.
const textList = ["1","2","3","Testing","text","to","particles"];
var textPos = 0;
function createParticles(text){
createTextMap(
text, // text to display
40, // font size
"Arial", // font
{ // style fot rendering font
fillStyle : "#6AF",
strokeStyle : "#F80",
lineWidth : 2,
lineJoin : "round",
},{ // bounding box to find a best fit for
top : 0,
left : 0,
width : canvas.width,
height : canvas.height,
}
)
}
// This function starts the animations
var started = false;
function startIt(){
started = true;
const next = ()=>{
var text = textList[(textPos++ ) % textList.length];
particles.mouseFX.dist = canvas.height / 8;
createParticles(text);
setTimeout(moveOut,text.length * 100 + 3000);
}
const moveOut = ()=>{
particles.moveOut();
setTimeout(next,2000);
}
setTimeout(next,0);
}
function setStyle(ctx,style){
Object.keys(style).forEach(key => ctx[key] = style[key]);
}
// the following function create the particles from text using a canvas
// the canvas used is dsplayed on the main canvas top left fro referance.
var tCan = createImage(100, 100); // canvas used to draw text
function createTextMap(text,size,font,style,fit){
// function to conver to colour hex value
const hex = (v)=> (v < 16 ? "0" : "") + v.toString(16);
// set up font so we can find the size.
tCan.ctx.font = size + "px " + font;
// get size of text
var width = Math.ceil(tCan.ctx.measureText(text).width + size);
// resize the canvas to fit the text
tCan.width = width;
tCan.height = Math.ceil(size *1.2);
// c is alias for context
var c = tCan.ctx;
// set up font
c.font = size + "px " + font;
c.textAlign = "center";
c.textBaseline = "middle";
// set style
setStyle(c,style);
// only do stroke and fill if they are set in styles object
if(style.strokeStyle){
c.strokeText(text, width / 2, tCan.height / 2);
}
if(style.fillStyle){
c.fillText(text, width / 2, tCan.height/ 2);
}
// prep the particles
particles.empty();
// get the pixel data
var data = c.getImageData(0,0,width,tCan.height).data;
var x,y,ind,rgb,a;
// find pixels with alpha > 128
for(y = 0; y < tCan.height; y += 1){
for(x = 0; x < width; x += 1){
ind = (y * width + x) << 2; // << 2 is equiv to * 4
if(data[ind + 3] > 128){ // is alpha above half
rgb = `#${hex(data[ind ++])}${hex(data[ind ++])}${hex(data[ind ++])}`;
// add the particle
particles.add(Vec(x, y), Vec(x, y), rgb);
}
}
}
// scale the particles to fit bounding box
var scale = Math.min(fit.width / width, fit.height / tCan.height);
particles.each(p=>{
p.home.x = ((fit.left + fit.width) / 2) + (p.home.x - (width / 2)) * scale;
p.home.y = ((fit.top + fit.height) / 2) + (p.home.y - (tCan.height / 2)) * scale;
})
.findCenter() // get center used to move particles on and off of screen
.moveOffscreen() // moves particles off the screen
.moveIn(); // set the particles to move into view.
}
// vector object a quick copy from other code.
function Vec(x,y){ // because I dont like typing in new
return new _Vec(x,y);
}
function _Vec(x = 0,y = 0){
this.x = x;
this.y = y;
return this;
}
_Vec.prototype = {
setAs(vec){
this.x = vec.x;
this.y = vec.y;
},
toString(){
return `vec : { x : ${this.x}, y : ${this.y} );`
}
}
// basic particle
const particle = {
pos : null,
delta : null,
home : null,
col : "black",
}
// array of particles
const particles = {
items : [], // actual array of particles
mouseFX : { // mouse FX
power : 20,
dist : 100,
curve : 3, // polynomial power
on : true,
},
fx : {
speed : 0.4,
drag : 0.15,
size : 4,
jiggle : 8,
},
// direction 1 move in -1 move out
direction : 1,
moveOut(){this.direction = -1; return this},
moveIn(){this.direction = 1; return this},
length : 0, // Dont touch this from outside particles.
each(callback){ // custom iteration
for(var i = 0; i < this.length; i++){
callback(this.items[i],i);
}
return this;
},
empty(){ // empty but dont dereference
this.length = 0;
return this;
},
deRef(){ // call to clear memory
this.items.length = 0;
this.length = 0;
},
add(pos,home,col){ // adds a particle
var p;
if(this.length < this.items.length){
p = this.items[this.length++];
// p.pos.setAs(pos);
p.home.setAs(home);
p.delta.x = 0;
p.delta.y = 0;
p.col = col;
}else{
this.items.push(
Object.assign(
{},
particle,
{
pos,
home,
col,
delta : Vec()
}
)
);
this.length = this.items.length
}
return this;
},
draw(){ // draws all
var p, size, sizeh;
sizeh = (size = this.fx.size) / 2;
for(var i = 0; i < this.length; i++){
p = this.items[i];
ctx.fillStyle = p.col;
ctx.fillRect(p.pos.x - sizeh, p.pos.y - sizeh, size, size);
}
},
update(){ // update all particles
var p,x,y,d;
var mP = this.mouseFX.power;
var mD = this.mouseFX.dist;
var mC = this.mouseFX.curve;
var fxJ = this.fx.jiggle;
var fxD = this.fx.drag;
var fxS = this.fx.speed;
for(var i = 0; i < this.length; i++){
p = this.items[i];
p.delta.x += (p.home.x - p.pos.x ) * fxS + (Math.random() - 0.5) * fxJ;
p.delta.y += (p.home.y - p.pos.y ) * fxS + (Math.random() - 0.5) * fxJ;
p.delta.x *= fxD;
p.delta.y *= fxD;
p.pos.x += p.delta.x * this.direction;
p.pos.y += p.delta.y * this.direction;
if(this.mouseFX.on){
x = p.pos.x - mouse.x;
y = p.pos.y - mouse.y;
d = Math.sqrt(x * x + y * y);
if(d < mD){
x /= d;
y /= d;
d /= mD;
d = (1-Math.pow(d,mC)) * mP;
p.pos.x += x * d;
p.pos.y += y * d;
}
}
}
return this;
},
findCenter(){ // find the center of particles maybe could do without
var x,y;
y = x = 0;
this.each(p => {
x += p.home.x;
y += p.home.y;
});
this.center = Vec(x / this.length, y / this.length);
return this;
},
moveOffscreen(){ // move start pos offscreen
var dist,x,y;
dist = Math.sqrt(this.center.x * this.center.x + this.center.y * this.center.y);
this.each(p => {
var d;
x = p.home.x - this.center.x;
y = p.home.y - this.center.y;
d = Math.max(0.0001,Math.sqrt(x * x + y * y)); // max to make sure no zeros
p.pos.x = p.home.x + (x / d) * dist;
p.pos.y = p.home.y + (y / d) * dist;
});
return this;
},
}
function onResize(){ // called from boilerplate
if(!started){
startIt();
}
}
/** SimpleFullCanvasMouse.js begin **/
// the following globals are available
// w, h, cw, ch, width height centerWidth centerHeight of canvas
// canvas, ctx, mouse, globalTime
//MAIN animation loop
function display() { // call once per frame
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
if(tCan){
// ctx.drawImage(tCan,0,0);
}
particles.update();
particles.draw();
}
/******************************************************************************
The code from here down is generic full page mouse and canvas boiler plate
code. As I do many examples which all require the same mouse and canvas
functionality I have created this code to keep a consistent interface. The
Code may or may not be part of the answer.
This code may or may not have ES6 only sections so will require a transpiler
such as babel.js to run on legacy browsers.
*****************************************************************************/
// V2.0 ES6 version for Stackoverflow and Groover QuickRun
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0;
// You can declare onResize (Note the capital R) as a callback that is also
// called once at start up. Warning on first call canvas may not be at full
// size.
;(function(){
const RESIZE_DEBOUNCE_TIME = 100;
var resizeTimeoutHandle;
var firstRun = true;
function createCanvas () {
var c,cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
function resizeCanvas () {
if (canvas === undefined) { canvas = createCanvas() }
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals() }
if (typeof onResize === "function") {
clearTimeout(resizeTimeoutHandle);
if (firstRun) { onResize() }
else { resizeTimeoutHandle = setTimeout(onResize, RESIZE_DEBOUNCE_TIME) }
firstRun = false;
}
}
function setGlobals () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) { e.preventDefault() }
var m; // alias for mouse
var mouse = {
x : 0, y : 0, w : 0, // mouse position and wheel
alt : false, shift : false, ctrl : false, // mouse modifiers
buttonRaw : 0,
over : false, // true if mouse over the element
buttonOnMasks : [0b1, 0b10, 0b100], // mouse button on masks
buttonOffMasks : [0b110, 0b101, 0b011], // mouse button off masks
active : false,
bounds : null,
eventNames : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(","),
event(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.buttonOnMasks[e.which - 1] }
else if (t === "mouseup") { m.buttonRaw &= m.buttonOffMasks[e.which - 1] }
else if (t === "mouseout") { m.over = false }
else if (t === "mouseover") { m.over = true }
else if (t === "mousewheel") {m.w = e.wheelDelta }
else if (t === "DOMMouseScroll") { m.w = -e.detail }
if (m.callbacks) { m.callbacks.forEach(c => c(e)) }
if ((m.buttonRaw & 2) && m.crashRecover !== null) {
if (typeof m.crashRecover === "function") { setTimeout(m.crashRecover, 0) }
}
e.preventDefault();
},
addCallback(callback) {
if (typeof callback === "function") {
if (m.callbacks === undefined) { m.callbacks = [callback] }
else { m.callbacks.push(callback) }
}
},
start(element) {
if (m.element !== undefined) { m.remove() }
m.element = element === undefined ? document : element;
m.eventNames.forEach(name => document.addEventListener(name, mouse.event) );
document.addEventListener("contextmenu", preventDefault, false);
m.active = true;
},
remove() {
if (m.element !== undefined) {
m.eventNames.forEach(name => document.removeEventListener(name, mouse.event) );
document.removeEventListener("contextmenu", preventDefault);
m.element = m.callbacks = undefined;
m.active = false;
}
}
}
m = mouse;
return mouse;
})();
function done() { // Clean up. Used where the IDE is on the same page.
window.removeEventListener("resize", resizeCanvas)
mouse.remove();
document.body.removeChild(canvas);
canvas = ctx = mouse = undefined;
}
function update(timer) { // Main update loop
if(ctx === undefined){ return }
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
setTimeout(function(){
canvas = createCanvas();
mouse.start(canvas, true);
resizeCanvas();
if(typeof Groover !== "undefined" && Groover.ide){ mouse.crashRecover = done }; // Requires Ace.js and GrooverDev.js. Prevents
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();
/** SimpleFullCanvasMouse.js end **/
/** CreateImage.js begin **/
// creates a blank image with 2d context
function createImage(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}
/** CreateImage.js end **/
canvas {
background : black;
}
well after all the attempts like hell I finally got my answer thanks to moáois answer to my post using particle slider. here is the code. Thanks everyone for your help :)

HTML5 Canvas Scallop Shape

I really need help badly on how to create a scallop shape using Canvas
I tried playing around the cloud sample but it was really difficult for me to create what I've wanted.
I simply wanted to know the code for the scallop shape for rectangle and circle.
This is the image that What I've wanted.
It design doesn't have to exactly the same but as possible it does look like this.
THANK YOU SO MUCH IN ADVANCEEE..
You can draw such a shape by using dotted line dash, like this(a bit tricky).
JavaScript:
const canvas = document.querySelector("#canvas");
canvas.width = canvas.height = 300;
const ctx = canvas.getContext("2d");
const rect = [50, 50, 200, 200];
//draw dotted line dash.
ctx.lineCap = "round";
ctx.setLineDash([0, 40]);
ctx.lineDashOffset = 20;
ctx.lineWidth = 42;
ctx.strokeStyle = "purple";
ctx.strokeRect(...rect);
//remove disuse range.
ctx.globalCompositeOperation = "destination-out";
ctx.lineWidth = 38;
ctx.strokeRect(...rect);
ctx.fillRect(...rect);
Demo:
http://jsdo.it/defghi1977/iFR7
From an older answer but the question was very vague and has a lot of extra baggage. Here is a snippet from that answer. It has some extra code in it that may be helpful but not directly related.
The function display (about halfway down) does most of the work, adding the arcs to the object box.
See running demo for instruction
const pointSize = 4;
const pointCol = "#4AF";
var arcDepth = -0.5; // depth of arc as a factor of line seg length
// Note to have arc go the other (positive) way you have
// to change the ctx.arc draw call by adding anticlockwise flag
// see drawArc for more
const arcCol = "#F92";
const arcWidth = 8;
// Find a circle that fits 3 points.
function fitCircleTo3P(p1x, p1y, p2x, p2y, p3x, p3y, arc) {
var vx,
vy,
c,
c1,
u;
c = (p2x - p1x) / (p1y - p2y); // slope of vector from vec 1 to vec 2
c1 = (p3x - p2x) / (p2y - p3y); // slope of vector from vec 2 to vec 3
// This will not happen in this example
if (c === c1) { // if slope is the same they must be on the same line
return null; // points are in a line
}
// locate the center
if (p1y === p2y) { // special case with p1 and p2 have same y
vx = (p1x + p2x) / 2;
vy = c1 * vx + (((p2y + p3y) / 2) - c1 * ((p2x + p3x) / 2));
} else
if (p2y === p3y) { // special case with p2 and p3 have same y
vx = (p2x + p3x) / 2;
vy = c * vx + (((p1y + p2y) / 2) - c * ((p1x + p2x) / 2));
} else {
vx = ((((p2y + p3y) / 2) - c1 * ((p2x + p3x) / 2)) - (u = ((p1y + p2y) / 2) - c * ((p1x + p2x) / 2))) / (c - c1);
vy = c * vx + u;
}
arc.x = vx;
arc.y = vy;
vx = p1x - vx;
vy = p1y - vy;
arc.rad = Math.sqrt(vx * vx + vy * vy);
return arc;
}
var points = [];
var arcs = [];
function addArc(p1, p2, depth) {
var arc = {
p1 : p1,
p2 : p2,
depth : depth,
rad : null, // radius
a1 : null, // angle from
a2 : null, // angle to
x : null,
y : null,
}
arcs.push(arc);
return arc;
}
function calcArc(arc, depth) {
var p = points[arc.p1]; // get points
var pp = points[arc.p2];
// change depth if needed
depth = arc.depth = depth !== undefined ? depth : arc.depth;
var vx = pp[0] - p[0]; // vector from p to pp
var vy = pp[1] - p[1];
var cx = (pp[0] + p[0]) / 2; // center point
var cy = (pp[1] + p[1]) / 2; // center point
var len = Math.sqrt(vx * vx + vy * vy); // get length
cx -= vy * depth; // find 3 point at 90 deg to line and dist depth
cy += vx * depth;
// To have depth as a fixed length uncomment 4 lines below and comment out 2 lines above.
//var nx = vx / len; // normalise vector
//var ny = vy / len;
//cx -= ny * depth; // find 3 point at 90 deg to line and dist depth
//cy += nx * depth;
fitCircleTo3P(p[0], p[1], cx, cy, pp[0], pp[1], arc); // get the circle that fits
arc.a1 = Math.atan2(p[1] - arc.y, p[0] - arc.x); // get angle from circle center to first point
arc.a2 = Math.atan2(pp[1] - arc.y, pp[0] - arc.x); // get angle from circle center to second point
}
function addPoint(x, y) {
points.push([x, y]);
}
function drawPoint(x, y, size, col) {
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
}
function drawArcStart(width,col){
ctx.lineCap = "round";
ctx.strokeStyle = col;
ctx.lineJoin = "round";
ctx.lineWidth = width;
ctx.beginPath();
}
function drawArc(arc){
ctx.arc(arc.x,arc.y,arc.rad,arc.a1,arc.a2);
}
function drawArcDone(){
ctx.closePath();
ctx.stroke();
}
function findClosestPoint(x, y, dist) {
var index = -1;
for (var i = 0; i < points.length; i++) {
var p = points[i];
var vx = x - p[0];
var vy = y - p[1];
var d = Math.sqrt(vx * vx + vy * vy);
if (d < dist) {
dist = d;
index = i;
}
}
return index;
}
var dragging = false;
var drag = -1;
var dragX, dragY;
var recalcArcs = false;
var box;
//========================================================================
// New box code from here down
// creates the box when canvas is ready
var onResize = function(){
box = {
x : canvas.width * (1/8),
y : canvas.height * (1/8),
w : canvas.width * (6/8),
h : canvas.height * (6/8),
recalculate : true,
arcCount : 20, // number of arcs to try and fit. Does not mean that it will happen
}
}
function display() {
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
if(mouse.w !== 0){
if(mouse.buttonRaw & 4){ // change arc depth
if(mouse.w < 0){
arcDepth *= 1/1.05;
}else{
arcDepth *= 1.05;
}
recalcArcs = true;
}else{ // change arc count
box.arcCount += Math.sign(mouse.w);
box.arcCount = Math.max(4,box.arcCount);
box.recalculate = true;
}
mouse.w = 0;
}
// drag out box;
if(mouse.buttonRaw & 1){
if(!dragging){
box.x = mouse.x;
box.y = mouse.y;
dragging = true;
}
box.w = mouse.x - box.x;
box.h = mouse.y - box.y;
box.recalculate = true;
if(box.w <0){
box.x = box.x + box.w;
box.w = - box.w;
}
if(box.h <0){
box.y = box.y + box.h;
box.h = - box.h;
}
}else{
dragging = false;
}
// stop error
if(box.w === 0 || box.h === 0){
box.recalculate = false;
}
// calculate box arcs
if(box.recalculate){
// reset arrays
points.length = 0;
arcs.length = 0;
// get perimeter length
var perimLen = (box.w + box.h)* 2;
// get estimated step size
var step = perimLen / box.arcCount;
// get inset size for width and hight
var wInStep = (box.w - (Math.floor(box.w/step)-1)*step) / 2;
var hInStep = (box.h - (Math.floor(box.h/step)-1)*step) / 2;
// fix if box to narrow
if(box.w < step){
wInStep = 0;
hInStep = 0;
step = box.h / (Math.floor(box.h/step));
}else if(box.h < step){
wInStep = 0;
hInStep = 0;
step = box.w / (Math.floor(box.w/step));
}
// Add points clock wise
var x = box.x + wInStep;
while(x < box.x + box.w){ // across top
addPoint(x,box.y);
x += step;
}
var y = box.y + hInStep;
while(y < box.y + box.h){ // down right side
addPoint(box.x + box.w,y);
y += step;
}
x = box.x + box.w - wInStep;
while(x > box.x){ // left along bottom
addPoint(x,box.y + box.h);
x -= step;
}
var y = box.y + box.h - hInStep;
while(y > box.y){ // up along left side
addPoint(box.x,y);
y -= step;
}
// calculate arcs.
for(var i =0; i <points.length; i++){
calcArc(addArc(i,(i + 1) % points.length,arcDepth));
}
box.recalculate = false;
}
// recalculate arcs if needed
for(var i = 0; i < arcs.length; i ++){
if(recalcArcs){
calcArc(arcs[i],arcDepth);
}
}
// draw arcs
drawArcStart(arcWidth,arcCol)
for(var i = 0; i < arcs.length; i ++){
drawArc(arcs[i]);
}
drawArcDone();
recalcArcs = false;
}
//===========================================================================================
// END OF ANSWER
// Boiler plate code from here down. Does mouse,canvas,resize and what not
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true; ;
(function () {
const RESIZE_DEBOUNCE_TIME = 100;
var createCanvas,
resizeCanvas,
setGlobals,
resizeCount = 0;
createCanvas = function () {
var c,
cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if (firstRun) {
onResize();
firstRun = false;
} else {
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
w : 0,
alt : false,
shift : false,
ctrl : false,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
crashRecover : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left;
m.y = e.pageY - m.bounds.top;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
} else if (t === "mousewheel") {
m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") {
m.w = -e.detail;
}
e.preventDefault();
}
m.start = function (element) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
m.element.removeEventListener("contextmenu", preventDefault);
m.element = m.callbacks = undefined;
m.active = false;
}
}
return mouse;
})();
function update(timer) { // Main update loop
if (ctx === undefined) {
return;
}
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
setTimeout(function () {
resizeCanvas();
mouse.start(canvas, true);
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
}, 0);
})();
Left click drag to create a box<br>Mouse wheel to change arc count<br>Hold right button down and wheel to change arc depth.<br>
Use https://www.w3schools.com/tags/canvas_beziercurveto.asp "Bezier Curve Method" to make complicated shapes.
I suggest going on desmos and messing around with the bezier curve in order to understand the complications. I hope this helped :)
Edit: Bezier curves work like this:
ctx.bezierCurveTo(Control point x, control point y, 2nd control point x, 2nd control point y, finishing x, finishing y);

Drag and drop on a canvas HTML5

I am trying to implement a drag and drop on a canvas representing 3 disks.
I would like to change with mouse the position of each mass. My main problem is that I am constrained by the length of axe for each of these 3 spheres.
For the moment, I have implemented the following function when mouse is moving inside the canvas (value of indexMass indicates which mass is moved: 1, 2 or 3 and t1, t2, t3 represents respectively the angle of mass 1, 2, 3):
// Happens when the mouse is moving inside the canvas
function myMove(event) {
if (isDrag) {
var x = event.offsetX;
var y = event.offsetY;
if (indexMass == 1)
{ // Update theta1 value
t1 = t1 + 0.1*Math.atan(y/x);
}
else if (indexMass == 2)
{ // Update theta2 value
t2 = t2 + 0.1*Math.atan(y/x);
}
else if (indexMass == 3)
{ // Update theta3 value
t3 = t3 + 0.1*Math.atan(y/x);
}
// Update drawing
DrawPend(canvas);
}
}
As you can see, I did for each angle:
t = t + 0.1*Math.atan(y/x);
with:
var x = event.offsetX;
var y = event.offsetY;
But this effect is not very nice. Once the sphere is selected with mouse (on mouse click), I would like the cursor to be stuck with this sphere or the sphere to follow the "delta" of the mouse coordinates when I am not on sphere any more.
Update 1
#Blindman67: thanks for your help, your code snippet is pretty complex for me, I didn't understand it all. But I am on the right way.
I am starting by the first issue: make rotate the selected disk with mouse staying very closed to it or over it, when dragging.
For the moment, I have modified my function myMove (which is called when I have clicked down and move the mouse for dragging) like:
// Happens when the mouse is moving inside the canvas
function myMove(event) {
// If dragging
if (isDrag) {
// Compute dx and dy before calling DrawPend
var lastX = parseInt(event.offsetX - mx);
var lastY = parseInt(event.offsetY - my);
var dx = lastX - window['x'+indexMass];
var dy = lastY - window['y'+indexMass];
// Change angle when dragging
window['t'+indexMass] = Math.atan2(dy, dx);
// Update drawing
DrawPend(canvas);
// Highlight dragging disk
fillDisk(indexMass, 'pink');
}
}
where indexMass is the index of dragged disk and window['x'+indexMass] , window['y'+indexMass] are the current coordinates of the selected disk center.
After, I compute the dx, dy respectively from coordinates mouse clicked when starting drag (mx, my returned by getMousePos function) and mouse coordinates with moving.
Finally, I change the angle of disk by set, for global variable (theta of selected disk), i.e window['t'+indexMass]:
// Change angle when dragging
window['t'+indexMass] = Math.atan2(dy, dx);
I have took your part of code with Math.atan2.
But the result of this function doesn't make a good animation with mouse dragging, I would like to know where this could come from.
Right now, I would like to implement only the dragging without modifying the length of axis, I will see more later for this functionality.
Update 2
I keep going on to find a solution about the dragging of a selected mass with mouse.
For trying a synthesis of what I have done previously, I believe the following method is good but this dragging method is not working very well: the selected disk doesn't follow correctly the mouse and I don't know why.
In myMove function (function called when I start dragging), I decided to:
Compute the dx, dy between the mouse coordinates and the selected disk coordinates, i.e:
var dx = parseInt(event.offsetX - window['x'+indexMass]);
var dy = parseInt(event.offsetY - window['y'+indexMass]);
indexMass represents the index of the selected disk.
Increment the position of selected disk (stored in temporary variables tmpX, tmpY) by dx, dy.
Compute the new angle theta (identified in code by global variable window['t'+indexMass]
Compute the new positions of selected disk with this new value of theta, i.e for example with disk1 (indexMass=1 and theta = t1):
x1= x0 +l1 * sin(t1)
y1= y0 +l1 * sin(t1)
I want to draw readers' attention to the fact that I want dragging with mouse not to modify the lengths of axes with mouse, this is a constraint.
Here's the entire myMove function (called when drag is starting) :
// Happens when the mouse is moving inside the canvas
function myMove(event) {
// If dragging
if (isDrag) {
console.log('offsetX', event.offsetX);
console.log('offsetY', event.offsetY);
var dx = parseInt(event.offsetX - window['x'+indexMass]);
var dy = parseInt(event.offsetY - window['y'+indexMass]);
console.log('dx', dx);
console.log('dy', dy);
// Temp variables
var tmpX = window['x'+indexMass];
var tmpY = window['y'+indexMass];
// Increment temp positions
tmpX += dx;
tmpY += dy;
// Compute new angle for indexMass
window['t'+indexMass] = Math.atan2(tmpX, tmpY);
console.log('printf', window['t'+indexMass]);
// Compute new positions of disks
dragComputePositions();
// Update drawing
DrawPend(canvas);
// Highlight dragging disk
fillDisk(indexMass, 'pink');
}
}
You can not move the OS mouse position. You can hide the mouse canvas.style.cursor = "none"; and then draw a mouse on the canvas your self but it will lag behind by one frame because when you get the mouse coordinates the OS has already placed the mouse at that position, and if you use requestAnimationFrame (RAF) the next presentation of the canvas will be at the next display refresh interval. If you don't use RAF you may or may not present the canvas on the current display refresh, but you will get occasional flicker and shearing.
To solve the problem (which is subjective) draw a line from the rotation point through the ball to the mouse position this will at least give the user some feedback as to what is happening.
I would also add some handles to the balls so you could change the mass (volume of sphere * density) and the length of axis.. The resize cursors are a problem as the will not match the direction of required movement when the angles have changes. You would need to find one closest to the correct angle or render a cursor to a canvas and use that.
Example code shows what I mean. (does not include sim) Move mouse over balls to move, when over you will also see two circles appear to change distance and radius (mass)
/*-------------------------------------------------------------------------------------
answer code
---------------------------------------------------------------------------------------*/
var balls = [];
var startX,startY;
var mouseOverBallIndex = -1;
var mouseOverDist = false;
var mouseOverMass = false;
const DRAG_CURSOR = "move";
const MASS_CURSOR = "ew-resize";
const DIST_CURSOR = "ns-resize";
var dragging = false;
var dragStartX = 0;
var dragStartY = 0;
function addBall(dist,radius){
balls.push({
dist : dist,
radius : Math.max(10,radius),
angle : -Math.PI / 2,
x : 0,
y : 0,
mass : (4/3) * radius * radius * radius * Math.PI,
});
}
function drawBalls(){
var i = 0;
var len = balls.length;
var x,y,dist,b,minDist,index,cursor;
ctx.lineWidth = 2;
ctx.strokeStyle = "black";
ctx.fillStyle = "blue"
ctx.beginPath();
x = startX;
y = startY;
ctx.moveTo(x, y)
for(; i < len; i += 1){
b = balls[i];
x += Math.cos(b.angle) * b.dist;
y += Math.sin(b.angle) * b.dist;
ctx.lineTo(x, y);
b.x = x;
b.y = y;
}
ctx.stroke();
minDist = Infinity;
index = -1;
for(i = 0; i < len; i += 1){
b = balls[i];
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fill();
if(!dragging){
x = b.x - mouse.x;
y = b.y - mouse.y;
dist = Math.sqrt(x * x + y * y);
if(dist < b.radius + 5 && dist < minDist){
minDist = dist;
index = i;
}
}
}
if(!dragging){
mouseOverBallIndex = index;
if(index !== -1){
cursor = DRAG_CURSOR;
b = balls[index];
ctx.fillStyle = "Red"
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fill();
dx = b.x - Math.cos(b.angle) * b.radius;
dy = b.y - Math.sin(b.angle) * b.radius;
x = dx - mouse.x;
y = dy - mouse.y;
dist = Math.sqrt(x * x + y * y);
ctx.beginPath();
if(dist < 6){
ctx.strokeStyle = "Yellow"
mouseOverDist = true;
ctx.arc(dx, dy, 12, 0, Math.PI * 2);
cursor = DIST_CURSOR;
}else{
ctx.strokeStyle = "black"
mouseOverDist = false;
ctx.arc(dx, dy, 5, 0, Math.PI * 2);
}
ctx.stroke();MASS_CURSOR
dx = b.x - Math.cos(b.angle + Math.PI/2) * b.radius;
dy = b.y - Math.sin(b.angle + Math.PI/2) * b.radius;
x = dx - mouse.x;
y = dy - mouse.y;
dist = Math.sqrt(x * x + y * y);
ctx.beginPath();
if(dist < 6){
ctx.strokeStyle = "Yellow"
mouseOverMass = true;
ctx.arc(dx, dy, 12, 0, Math.PI * 2);
cursor = MASS_CURSOR;
}else{
ctx.strokeStyle = "black"
mouseOverMass = false;
ctx.arc(dx, dy, 5, 0, Math.PI * 2);
}
ctx.stroke();
canvas.style.cursor = cursor;
}else{
canvas.style.cursor = "default";
}
}else{
b = balls[mouseOverBallIndex];
ctx.fillStyle = "Yellow"
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fill();
}
}
function display(){ // put code in here
var x,y,b
if(balls.length === 0){
startX = canvas.width/2;
startY = canvas.height/2;
addBall((startY * 0.8) * (1/4), startY * 0.04);
addBall((startY * 0.8) * (1/3), startY * 0.04);
addBall((startY * 0.8) * (1/2), startY * 0.04);
}
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
if((mouse.buttonRaw & 1) && mouseOverBallIndex > -1){
b = balls[mouseOverBallIndex];
if(dragging === false){
dragging = true;
dragStartX = balls[mouseOverBallIndex].x;
dragStartY = balls[mouseOverBallIndex].y;
}else{
b = balls[mouseOverBallIndex];
if(mouseOverBallIndex === 0){
x = startX;
y = startY;
}else{
x = balls[mouseOverBallIndex-1].x
y = balls[mouseOverBallIndex-1].y
}
if(mouseOverDist){
var dist = Math.sqrt(Math.pow(x-mouse.x,2)+Math.pow(y-mouse.y,2));
b.dist = dist + b.radius;
}else
if(mouseOverMass){
var dist = Math.sqrt(Math.pow(dragStartX-mouse.x,2)+Math.pow(dragStartY-mouse.y,2));
b.radius = Math.max(10,dist);
b.mass = dist * dist * dist * (4/3) * Math.PI;
}else{
b.angle = Math.atan2(mouse.y - y, mouse.x - x);
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = "grey";
ctx.moveTo(x,y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
}
}
}else if(dragging){
dragging = false;
}
drawBalls();
}
/*-------------------------------------------------------------------------------------
answer code END
---------------------------------------------------------------------------------------*/
/** SimpleFullCanvasMouse.js begin **/
const CANVAS_ELEMENT_ID = "canv";
const U = undefined;
var w, h, cw, ch; // short cut vars
var canvas, ctx, mouse;
var globalTime = 0;
var createCanvas, resizeCanvas, setGlobals;
var L = typeof log === "function" ? log : function(d){ console.log(d); }
createCanvas = function () {
var c,cs;
cs = (c = document.createElement("canvas")).style;
c.id = CANVAS_ELEMENT_ID;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === U) { canvas = createCanvas(); }
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals(); }
}
setGlobals = function(){ cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2; balls.length = 0; }
mouse = (function(){
function preventDefault(e) { e.preventDefault(); }
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === U) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey; m.shift = e.shiftKey; m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1]; }
else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2]; }
else if (t === "mouseout") { m.buttonRaw = 0; m.over = false; }
else if (t === "mouseover") { m.over = true; }
else if (t === "mousewheel") { m.w = e.wheelDelta; }
else if (t === "DOMMouseScroll") { m.w = -e.detail; }
if (m.callbacks) { m.callbacks.forEach(c => c(e)); }
e.preventDefault();
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === U) { m.callbacks = [callback]; }
else { m.callbacks.push(callback); }
} else { throw new TypeError("mouse.addCallback argument must be a function"); }
}
m.start = function (element, blockContextMenu) {
if (m.element !== U) { m.removeMouse(); }
m.element = element === U ? document : element;
m.blockContextMenu = blockContextMenu === U ? false : blockContextMenu;
m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
if (m.blockContextMenu === true) { m.element.addEventListener("contextmenu", preventDefault, false); }
}
m.remove = function () {
if (m.element !== U) {
m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
if (m.contextMenuBlocked === true) { m.element.removeEventListener("contextmenu", preventDefault);}
m.element = m.callbacks = m.contextMenuBlocked = U;
}
}
return mouse;
})();
var done = function(){
window.removeEventListener("resize",resizeCanvas)
mouse.remove();
document.body.removeChild(canvas);
canvas = ctx = mouse = U;
L("All done!")
}
resizeCanvas(); // create and size canvas
mouse.start(canvas,true); // start mouse on canvas and block context menu
window.addEventListener("resize",resizeCanvas); // add resize event
function update(timer){ // Main update loop
globalTime = timer;
display(); // call demo code
// continue until mouse right down
if (!(mouse.buttonRaw & 2)) { requestAnimationFrame(update); } else { done(); }
}
requestAnimationFrame(update);
/** SimpleFullCanvasMouse.js end **/
(Posted a solution from the question author to move it to the answer space).
Problem solved! I forgot to take into account the position of "indexMass-1" disk to compute the new angle with Math.atan2 function.

Categories

Resources