Using a repeater instead of a canvas to curve text - javascript

Instead of using a canvas to curve the text to fit on the image. Is it possible instead to use a Repeater to achieve the same effect? Currently my code does what I need it to do with a Canvas. But I'm curious and it also might be better for me to use a Repeater to accomplish what I ultimately want. How can I modify my existing code to achieve this?
import QtQuick 2.15
import QtQuick.Window 2.15
Window {
id: root
visible: true
width: 640
height: 480
title: qsTr("Hello World")
property string letters: "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
Image {
id: abcBar
source: "alphabetBar.png"
visible: false
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -10
}
// Rectangle {
// id: body
// anchors.fill: parent
// readonly property real dx: 12
// function midpoint(idx) {
// return 10 + idx * body.dx;
// }
// Repeater {
// anchors.fill: parent
// model: root.letters.length
// Text {
// anchors.horizontalCenter: parent.left
// anchors.horizontalCenterOffset: body.midpoint(index)
// anchors.verticalCenter: parent.verticalCenter
// text: root.letters[index]
// }
// }
// }
Canvas {
anchors.fill: parent
onPaint: {
var ctx = getContext('2d');
ctx.save();
ctx.canvas.width = 160;
ctx.canvas.height = 432;
ctx.font = "18px Roboto";
ctx.textAlign = "center";
ctx.fillStyle = "#000000"
const centerX = 10;
const centerY = ctx.canvas.height / 2;
const angle = Math.PI;
const radius = 130
ctx.fillStyle = "#000000"
ctx.restore();
ctx.save();
ctx.drawImage(abcBar, 0, 0, abcBar.width, abcBar.height);
console.log('loaded')
ctx.restore();
const args = {
ctx,
// text: "A • D • G • J • M • P • S • V • Z",
text: root.letters,
offset: 0,
G1: {
x: 20,
y: 80,
},
G2: {
x: 190,
y: 230,
},
G3: {
x: 0,
y: 372
},
}
textOnCurve(args);
}
}
function textOnCurve({ ctx, text, offset, G1, G2, G3, G4}){
const x1 = G1.x;
const y1 = G1.y;
const x2 = G2.x;
const y2 = G2.y;
const x3 = G3.x;
const y3 = G3.y;
const x4 = G3.x;
const y4 = G3.y;
ctx.save();
ctx.textAlign = "center";
var widths = [];
for (var i = 0; i < text.length; i++)
{
widths[widths.length] = ctx.measureText(text[i]).width;
}
ctx.beginPath();
var ch = curveHelper(x1, y1, x2, y2, x3, y3, x4, y4);
ctx.stroke();
var pos = offset;
var cpos = 0;
for (var j = 0; j < text.length; j++)
{
pos += widths[j] / 2;
cpos = ch.forward(pos);
ch.tangent(cpos);
ctx.setTransform(ch.vect.x, ch.vect.y, -ch.vect.y, ch.vect.x, ch.vec.x, ch.vec.y);
ctx.fillText(text[j], 0, 0);
pos += widths[j] / 2;
}
ctx.restore();
}
function curveHelper(x1, y1, x2, y2, x3, y3, x4, y4)
{
var tx1, ty1, tx2, ty2, tx3, ty3, tx4, ty4;
var a, b, c, u;
var vec, currentPos, vec1, vect, quad, currentDist;
vec = { x: 0, y: 0 } ;
vec1 = { x: 0, y: 0 } ;
vect = { x: 0, y: 0 } ;
quad = false;
currentPos = 0;
currentDist = 0;
if (x4 === undefined || x4 === null)
{
quad = true;
x4 = x3;
y4 = y3;
}
var estLen = Math.sqrt((x4 - x1)* (x4 - x1)+ (y4 - y1)* (y4 - y1));
var onePix = 1 / estLen;
function posAtC(c)
{
tx1 = x1; ty1 = y1;
tx2 = x2; ty2 = y2;
tx3 = x3; ty3 = y3;
tx1 += (tx2 - tx1)* c;
ty1 += (ty2 - ty1)* c;
tx2 += (tx3 - tx2)* c;
ty2 += (ty3 - ty2)* c;
tx3 += (x4 - tx3)* c;
ty3 += (y4 - ty3)* c;
tx1 += (tx2 - tx1)* c;
ty1 += (ty2 - ty1)* c;
tx2 += (tx3 - tx2)* c;
ty2 += (ty3 - ty2)* c;
vec.x = tx1 + (tx2 - tx1)* c;
vec.y = ty1 + (ty2 - ty1)* c;
return vec;
}
function posAtQ(c)
{
tx1 = x1; ty1 = y1;
tx2 = x2; ty2 = y2;
tx1 += (tx2 - tx1)* c;
ty1 += (ty2 - ty1)* c;
tx2 += (x3 - tx2)* c;
ty2 += (y3 - ty2)* c;
vec.x = tx1 + (tx2 - tx1)* c;
vec.y = ty1 + (ty2 - ty1)* c;
return vec;
}
function forward(dist)
{
var step;
helper.posAt(currentPos);
while (currentDist < dist)
{
vec1.x = vec.x;
vec1.y = vec.y;
currentPos += onePix;
helper.posAt(currentPos);
currentDist += step = Math.sqrt((vec.x - vec1.x)* (vec.x - vec1.x)+ (vec.y - vec1.y)* (vec.y - vec1.y));
}
currentPos -= ((currentDist - dist)/ step)* onePix
currentDist -= step;
helper.posAt(currentPos);
currentDist += Math.sqrt((vec.x - vec1.x)* (vec.x - vec1.x)+ (vec.y - vec1.y)* (vec.y - vec1.y));
return currentPos;
}
function tangentQ(pos)
{
a = (1 - pos)* 2;
b = pos * 2;
vect.x = a * (x2 - x1)+ b * (x3 - x2);
vect.y = a * (y2 - y1)+ b * (y3 - y2);
u = Math.sqrt(vect.x * vect.x + vect.y * vect.y);
vect.x /= u;
vect.y /= u;
}
function tangentC(pos)
{
a = (1 - pos)
b = 6 * a * pos;
a *= 3 * a;
c = 3 * pos * pos;
vect.x = -x1 * a + x2 * (a - b)+ x3 * (b - c)+ x4 * c;
vect.y = -y1 * a + y2 * (a - b)+ y3 * (b - c)+ y4 * c;
u = Math.sqrt(vect.x * vect.x + vect.y * vect.y);
vect.x /= u;
vect.y /= u;
}
var helper = {
vec: vec,
vect: vect,
forward: forward,
}
if (quad)
{
helper.posAt = posAtQ;
helper.tangent = tangentQ;
} else
{
helper.posAt = posAtC;
helper.tangent = tangentC;
}
return helper
}
}

That is certainly possible:
Rectangle {
id: control
width: Math.min(parent.height, parent.width)
height: width
anchors.centerIn: parent
color: "#40ff0000" //to see where it is going, which helps in positioning
property double startAngle: -45
property double endAngle: 45
Repeater {
model: 26
Text {
property double angle: control.startAngle + (control.endAngle - control.startAngle) * (index / 26)
x: (control.width / 2) * (1 + 0.9 * Math.cos(Math.PI * angle / 180)) - width * 0.5
y: (control.height / 2) * (1 + 0.9 * Math.sin(Math.PI * angle / 180)) - height * 0.5
text: String.fromCharCode(index + 65)
//rotation: angle
}
}
}
In the occasion you want to add a MouseArea to each text or change it to a button, this would also be a wise route to go.
Note that you will have to position the rectangle a bit with respect to your image.

Related

How to calculate all pixels on the circumference of an ellipse?

I am drawing an ellipse in a "canvas" element with the following lines of code:
let centerX = 250, centerY = 250;
let widthEllipse = 75;
let heightEllipse = 50;
context.beginPath();
context.lineWidth = 1;
context.ellipse(centerX, centerY, heightEllipse, widthEllipse, Math.PI / 4, 0, 2 * Math.PI);
context.stroke();
As a result, I get this drawing:
How can I calculate all the pixels on the circumference of an ellipse?
For example, to make such calculations for a circle, I used the following formulas:
for (let y = 0; y < r*2; y++) {
P1=(x0-sqrt(r^2-(y-y0)^2), y);
P2=(x0+sqrt(r^2-(y-y0)^2), y;
}
Scan lines. Axis Aligned ellipse
The first example is simple and only handles axis aligned ellipses.
Call is scanEllipse(x, y, xRadius, yRadius);
const ctx = can.getContext("2d");
scanEllipse(102, 64, 100, 30);
scanEllipse(256, 64, 40, 60);
function scanEllipse(x, y, h, v) {
const hSqr = h * h;
const scale = h / v;
var i = -v;
while (i <= v) {
var ii = i * scale;
var p1 = (hSqr - ii * ii) ** 0.5;
ctx.fillStyle = i % 2 ? "#F00" : "#000";
ctx.fillRect(x - p1, y + i, p1 * 2, 1);
i++;
}
}
canvas { border: 1px solid black; }
<canvas id="can" width="300" height="128"></canvas>
Scan lines. Rotated ellipse
This gets messy. To make sure it covers as many cases as possible I animated the function. I could not see any glitches but there may be some cases (very big ellipse or ellipses with very large eccentricity) where the floating point error may cause artifacts.
Call is scanEllipse(x, y, xRadius, yRadius, ang); ang is in radians.
const ctx = can.getContext("2d");
const quadRoots = (a, b, c) => { // find quadratic roots
if (Math.abs(a) < 1e-6) { return b != 0 ? [-c / b] : [] }
b /= a;
var d = b * b - 4 * (c / a);
if (d > 0) {
d = d ** 0.5;
return [0.5 * (-b + d), 0.5 * (-b - d)]
}
return d === 0 ? [0.5 * -b] : [];
}
function drawHLine(x, y, w) {
ctx.fillStyle = y % 2 ? "#F00" : "#000";
ctx.fillRect(x, y, w, 1);
}
function scanEllipse(x, y, h, v, a) {
const C = Math.cos(a), C2 = C * C;
const S = Math.sin(a), S2 = S * S;
const v2 = v * v, h2 = h * h;
const A = v2 * C2 + h2 * S2;
var i = 0, a, b, scan = true;
function atY(y) {
const B = 2 * y * C * S * (v2 - h2);
const c = y * y *(v2 * S2 + h2 * C2 )- h2 * v2;
return quadRoots(A, B, c);
}
while (scan) {
[a, b] = atY(i);
if (a !== undefined && b !== undefined) {
drawHLine(x + a, y + i, b - a);
if (i > 0) {
[a, b] = atY(-i);
drawHLine(x + a, y - i, b - a);
}
} else { scan = false; }
i++;
}
}
requestAnimationFrame(renderLoop);
function renderLoop(time) {
ctx.clearRect(0, 0, can.width, can.height);
const h = Math.sin(time * 0.001) * 45 + 50;
const v = Math.sin(time * 0.00333) * 35 + 40;
scanEllipse(100, 100, h, v, time * 0.00077);
requestAnimationFrame(renderLoop);
}
canvas { border: 1px solid black; }
<canvas id="can" width="200" height="200"></canvas>
Scan lines. Rotated ellipse edge only
Addresses just the outside pixels. (to fit the rules of good pixel art line work).
The function uses the same method as above by uses each previous row to workout which pixels are edge pixels.
There is plenty of room for optimization and the animation is slowed just a little to let use see the pixels.
Not that this version calculates the left and right edge using the center of each pixel row (eg y + 0.5). Using the top or bottom of the row makes for a lesser quality ellipse IMHO.
Call is scanEllipse(x, y, xRadius, yRadius, ang); ang is in radians.
const ctx = can.getContext("2d");
const quadRoots = (a, b, c) => { // find quadratic roots
if (Math.abs(a) < 1e-6) { return b != 0 ? [-c / b] : [] }
b /= a;
var d = b * b - 4 * (c / a);
if (d > 0) {
d = d ** 0.5;
return [0.5 * (-b + d), 0.5 * (-b - d)]
}
return d === 0 ? [0.5 * -b] : [];
}
function drawHLine(x, y, w) {
ctx.fillStyle = y % 2 ? "#F00" : "#000";
ctx.fillRect(x, y, w, 1);
}
function scanEllipse(x, y, h, v, a) {
const C = Math.cos(a), C2 = C * C;
const S = Math.sin(a), S2 = S * S;
const v2 = v * v, h2 = h * h;
const A = v2 * C2 + h2 * S2;
var i = 0, a1, b1, a2, b2, scan = true;
var pa1, pb1, pa2, pb2; // p for previous
function atY(y) {
const B = 2 * y * C * S * (v2 - h2);
const c = y * y *(v2 * S2 + h2 * C2 )- h2 * v2;
return quadRoots(A, B, c);
}
const max = Math.max, min = Math.min;
const addPx = (x, y) => ctx.fillRect(x, y, 1, 1);
const addEdgeLine = (x1, x2, y) => {
[x1, x2] = [min(x1, x2) | 0, max(x1, x2) | 0];
if (x1 == x2) { addPx(x1++, y); }
while (x1 < x2) {addPx(x1++, y);}
}
while (scan) {
[a1, b1] = atY(i - 0.5);
if (a1 !== undefined && b1 !== undefined) {
[a2, b2] = atY(-i +0.5);
if (pa1) {
addEdgeLine(pa1 + x, a1 + x, y + i - 1);
addEdgeLine(pb1 + x, b1 + x, y + i - 1);
if (i > 1) {
addEdgeLine(pa2 + x, a2 + x, y - i + 1);
addEdgeLine(pb2 + x, b2 + x, y - i + 1);
}
pa2 = a2;
pb2 = b2;
} else {
pa2 = min(a1,a2);
pb2 = max(b1,b2);
}
pa1 = a1;
pb1 = b1;
} else {
// add last row (top bottom)
if (pa1) {
addEdgeLine(pa1 + x, pb1 + x, y + i - 1);
addEdgeLine(pa2 + x, pb2 + x, y - i + 1);
}
scan = false;
}
i++;
}
}
requestAnimationFrame(renderLoop);
var tick = 0;
function renderLoop(time) {
if (tick++ % 4 === 0) {
time /= 4;
ctx.clearRect(0, 0, can.width, can.height);
const h = Math.sin(time * 0.001) ** 2 * 34 + 10;
const v = Math.sin(time * 0.00333) ** 2 * 35 + 10;
scanEllipse(50, 50, h, v, time * 0.00077);
}
requestAnimationFrame(renderLoop);
}
canvas {
border: 1px solid black;
width: 400px;
height: 400px;
image-rendering: pixelated;
}
<canvas id="can" width="100" height="100"></canvas>

Properly inject JavaScript functions inside of the QT project

I created the below image in html and JavaScript where the alphabets curve around the arc of the image. I am trying to convert the code I wrote and properly inject the functions inside of a QT project. However, my current implementation does not work due TypeError: Type errors. Can someone help with this?
Also if possible: I would like to rotate the alphabets to be vertical to the arc instead of horizontal.
import QtQuick 2.15
import QtQuick.Window 2.15
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Canvas {
anchors.fill: parent
Image {
id: abcBar
source: "alphabetBar.png"
visible: false
}
onPaint: {
var ctx = getContext('2d');
ctx.save();
ctx.canvas.width = 160;
ctx.canvas.height = 432;
ctx.font = "18px Roboto";
ctx.textAlign = "center";
ctx.fillStyle = "#000000"
const centerX = 10;
const centerY = ctx.canvas.height / 2;
const angle = Math.PI;
const radius = 130
ctx.fillStyle = "#000000"
ctx.restore();
ctx.save();
ctx.drawImage(abcBar, 0, 0, ctx.canvas.width, ctx.canvas.height);
console.log('loaded')
ctx.restore();
const args = {
ctx,
text: "A • D • G • J • M • P • S • V • Z",
offset: 0,
G1: {
x: 20,
y: 80,
},
G2: {
x: 190,
y: 230,
},
G3: {
x: 0,
y: 372
},
}
textOnCurve(args);
}
}
function textOnCurve({ ctx, text, offset, G1, G2, G3, G4}){
const x1 = G1.x;
const y1 = G1.y;
const x2 = G2.x;
const y2 = G2.y;
const x3 = G3.x;
const y3 = G3.y;
const x4 = G3.x;
const y4 = G3.y;
ctx.save();
ctx.textAlign = "center";
var widths = [];
for (var i = 0; i < text.length; i++)
{
widths[widths.length] = ctx.measureText(text[i]).width;
}
ctx.beginPath();
var ch = curveHelper(x1, y1, x2, y2, x3, y3, x4, y4);
ctx.stroke();
var pos = offset;
var cpos = 0;
for (var j = 0; j < text.length; j++)
{
pos += widths[j] / 2;
cpos = ch.forward(pos);
ch.tangent(cpos);
ctx.setTransform(ch.vect.x, ch.vect.y, -ch.vect.y, ch.vect.x, ch.vec.x, ch.vec.y);
ctx.fillText(text[j], 0, 0);
pos += widths[j] / 2;
}
ctx.restore();
}
function curveHelper(x1, y1, x2, y2, x3, y3, x4, y4)
{
var tx1, ty1, tx2, ty2, tx3, ty3, tx4, ty4;
var a, b, c, u;
var vec, currentPos, vec1, vect, quad, currentDist;
vec = { x: 0, y: 0 } ;
vec1 = { x: 0, y: 0 } ;
vect = { x: 0, y: 0 } ;
quad = false;
currentPos = 0;
currentDist = 0;
if (x4 === undefined || x4 === null)
{
quad = true;
x4 = x3;
y4 = y3;
}
var estLen = Math.sqrt((x4 - x1)* (x4 - x1)+ (y4 - y1)* (y4 - y1));
var onePix = 1 / estLen;
function posAtC(c)
{
tx1 = x1; ty1 = y1;
tx2 = x2; ty2 = y2;
tx3 = x3; ty3 = y3;
tx1 += (tx2 - tx1)* c;
ty1 += (ty2 - ty1)* c;
tx2 += (tx3 - tx2)* c;
ty2 += (ty3 - ty2)* c;
tx3 += (x4 - tx3)* c;
ty3 += (y4 - ty3)* c;
tx1 += (tx2 - tx1)* c;
ty1 += (ty2 - ty1)* c;
tx2 += (tx3 - tx2)* c;
ty2 += (ty3 - ty2)* c;
vec.x = tx1 + (tx2 - tx1)* c;
vec.y = ty1 + (ty2 - ty1)* c;
return vec;
}
function posAtQ(c)
{
tx1 = x1; ty1 = y1;
tx2 = x2; ty2 = y2;
tx1 += (tx2 - tx1)* c;
ty1 += (ty2 - ty1)* c;
tx2 += (x3 - tx2)* c;
ty2 += (y3 - ty2)* c;
vec.x = tx1 + (tx2 - tx1)* c;
vec.y = ty1 + (ty2 - ty1)* c;
return vec;
}
function forward(dist)
{
var step;
helper.posAt(currentPos);
while (currentDist < dist)
{
vec1.x = vec.x;
vec1.y = vec.y;
currentPos += onePix;
helper.posAt(currentPos);
currentDist += step = Math.sqrt((vec.x - vec1.x)* (vec.x - vec1.x)+ (vec.y - vec1.y)* (vec.y - vec1.y));
}
currentPos -= ((currentDist - dist)/ step)* onePix
currentDist -= step;
helper.posAt(currentPos);
currentDist += Math.sqrt((vec.x - vec1.x)* (vec.x - vec1.x)+ (vec.y - vec1.y)* (vec.y - vec1.y));
return currentPos;
}
function tangentQ(pos)
{
a = (1 - pos)* 2;
b = pos * 2;
vect.x = a * (x2 - x1)+ b * (x3 - x2);
vect.y = a * (y2 - y1)+ b * (y3 - y2);
u = Math.sqrt(vect.x * vect.x + vect.y * vect.y);
vect.x /= u;
vect.y /= u;
}
function tangentC(pos)
{
a = (1 - pos)
b = 6 * a * pos;
a *= 3 * a;
c = 3 * pos * pos;
vect.x = -x1 * a + x2 * (a - b)+ x3 * (b - c)+ x4 * c;
vect.y = -y1 * a + y2 * (a - b)+ y3 * (b - c)+ y4 * c;
u = Math.sqrt(vect.x * vect.x + vect.y * vect.y);
vect.x /= u;
vect.y /= u;
}
var helper = {
vec: vec,
vect: vect,
forward: forward,
}
if (quad)
{
helper.posAt = posAtQ;
helper.tangent = tangentQ;
} else
{
helper.posAt = posAtC;
helper.tangent = tangentC;
}
return helper
}
}
The reference to Image on line 35 is not going to work because it is a browser-specific JavaScript class which is not available in the QML JavaScript runtime. Even though it seems like the QML Image class you have commented out should be referencable in the runtime, no QML components are directly referencable in JavaScript as classes like this.
You will need to port your code to use Canvas::loadImage() as documented here:
https://doc.qt.io/qt-5/qml-qtquick-canvas.html#loadImage-method
Or you can uncomment your QML Image object and call Context2d::drawImage() with its id as the first argument as documented here:
https://doc.qt.io/qt-5/qml-qtquick-context2d.html#drawImage-method-2
Note, if you do that, the Image will appear twice in the window unless you set visible: false on the QML one.

canvas event listener to switch argument state

Hi I'm trying to add interactivity to the canvas element.
below you can see the electric canvas. I wonder how can i add a mouse click event listener on the switch in order to open and close it. Is it possible to change the switch open argument from true to false on click ?
I could define a specific position and capture the mouse click event on that position but this circuit should be use by anybody to create his circuit so It isn't possible to give a predifined position to the switch
/*Basic Circuit symbol toolset, still alot missing
credit to: https://stackoverflow.com/users/434421/mindoftea*/
class Circuit {
constructor(name="canvas", ix=50, iy=50) {
this.canvas = document.getElementById(name);
this.ctx = canvas.getContext("2d");
this.d = 0;
this.ix = ix;
this.iy = iy;
this.cx = ix;
this.cy = iy;
this.dx = 1;
this.dy = 0;
this.px = 0;
this.py = 0;
this.pd = 0;
this.pdx = 0;
this.pdy = 0;
this.ctx.beginPath();
this.ctx.moveTo(this.cx, this.cy);
}
start(ix=10, iy=10) {
this.d = 0;
this.ix = ix;
this.iy = iy;
this.cx = ix;
this.cy = iy;
this.dx = 1;
this.dy = 0;
this.px = 0;
this.py = 0;
this.pd = 0;
this.pdx = 0;
this.pdy = 0;
this.ctx.beginPath();
this.ctx.moveTo(this.cx, this.cy);
}
finish(close=true) {
if (close) {
this.ctx.lineTo(this.ix, this.iy);
}
this.ctx.stroke();
}
save() {
this.px = this.cx;
this.py = this.cy;
this.pd = this.d;
this.pdx = this.dx;
this.pdy = this.dy;
}
restore() {
this.cx = this.px;
this.cy = this.py;
this.d = this.pd;
this.dx = this.pdx;
this.dy = this.pdy;
this.ctx.moveTo(this.cx, this.cy);
}
newelement(len=50) {
this.ctx.save();
this.ctx.translate(this.cx, this.cy);
this.ctx.rotate(this.d*Math.PI/2);
if (this.dx < -0.5 ) {
this.ctx.rotate(Math.PI);
this.ctx.translate(-len, 0);
}
this.ctx.moveTo(0,0);
}
endelement(len=50) {
this.ctx.restore();
this.cx += this.dx * len;
this.cy += this.dy * len;
this.ctx.moveTo(this.cx,this.cy);
}
wire(len=50) {
this.newelement(len);
this.ctx.lineTo(len,0);
this.endelement(len);
}
drawWire(len=50) {
this.cx += this.dx * len;
this.cy += this.dy * len;
this.ctx.lineTo(this.cx, this.cy);
}
power(len=50, n=2, label="") {
var space = 5;
var wl = (len - (2*n-1)*space)/2;
this.newelement(len);
this.ctx.lineTo(wl,0);
this.ctx.fillText(label, wl, 4*space);
while (n--) {
this.ctx.moveTo(wl+space*(2*n+1), -space);
this.ctx.lineTo(wl+space*(2*n+1), space);
this.ctx.moveTo(wl+space*(2*n), -2*space);
this.ctx.lineTo(wl+space*(2*n), 2*space);
}
this.ctx.moveTo(len-wl, 0);
this.ctx.lineTo(len,0);
this.endelement(len);
}
drawPower(len=50, n=2, label="") {
var space = 5;
var wl = (len - (2*n-1)*space)/2;
this.drawWire(wl);
this.ctx.fillText(label, this.cx + 30 * this.dy, this.cy + 30 * this.dx);
while (n--) {
this.ctx.moveTo(this.cx + 15 * this.dy, this.cy + 15 * this.dx);
this.ctx.lineTo(this.cx - 15 * this.dy, this.cy - 15 * this.dx);
this.cx += this.dx * space;
this.cy += this.dy * space;
this.ctx.moveTo(this.cx + 2*space * this.dy, this.cy + 2*space * this.dx);
this.ctx.lineTo(this.cx - 2*space * this.dy, this.cy - 2*space * this.dx);
if (n != 0) {
this.cx += this.dx * space;
this.cy += this.dy * space;
}
}
this.ctx.moveTo(this.cx, this.cy);
this.drawWire(wl);
}
capacitor(len=50, label="") {
var space=5;
var hh = space*1.8;
var cl = (len-space)/2;
this.newelement(len);
this.ctx.lineTo(cl,0);
this.ctx.fillText(label, cl, hh+10)
this.ctx.moveTo(cl, -hh);
this.ctx.lineTo(cl, hh);
this.ctx.moveTo(cl+space, -hh);
this.ctx.lineTo(cl+space, hh);
this.ctx.moveTo(cl+space, 0);
this.ctx.lineTo(len,0);
this.endelement(len);
}
drawCapacitor(len=50, label="") {
var space=5;
var cl = (len-space)/2;
this.drawWire(cl);
this.ctx.fillText(label, this.cx + 20 * this.dy, this.cy + 20 * this.dx )
this.ctx.moveTo(this.cx + 10 * this.dy, this.cy + 10 * this.dx);
this.ctx.lineTo(this.cx - 10 * this.dy, this.cy - 10 * this.dx);
this.cx += this.dx * space;
this.cy += this.dy * space;
this.ctx.moveTo(this.cx + 10 * this.dy, this.cy + 10 * this.dx);
this.ctx.lineTo(this.cx - 10 * this.dy, this.cy - 10 * this.dx);
this.ctx.moveTo(this.cx, this.cy);
this.drawWire(cl);
}
inductor(len=50, n=4, label="") {
var xs, ys;
xs = 1;
ys = 2;
var space = 6;
var wl = (len-(n+1)*space)/2;
this.newelement(len);
this.ctx.lineTo(wl, 0);
this.ctx.fillText(label, wl, 25);
this.ctx.scale(xs, ys);
while (n--) {
this.ctx.moveTo(wl+space*(n+2), 0);
this.ctx.arc(wl+space*(n+1), 0, space, 0, Math.PI, 1);
this.ctx.moveTo(wl+space*(n), 0);
if (n>0) {
this.ctx.arc(wl+space*(n+1/2), 0, space/2, Math.PI,0, 1);
}
}
this.ctx.scale(1/xs, 1/ys);
this.ctx.moveTo(len-wl,0);
this.ctx.lineTo(len,0);
this.endelement(len);
}
drawInductor(len=50, n=4, label="") {
var xs, ys;
xs = 1 + Math.abs(this.dy);
ys = 1 + Math.abs(this.dx);
var space = 6;
var wl = (len-n*space)/2;
this.drawWire(len);
this.ctx.fillText(label, this.cx+(10+space)*this.dy, this.cy+(10+space)*this.dx)
this.cx += this.dx * space;
this.cy += this.dy * space;
this.ctx.scale(xs, ys);
while (n--) {
//ctx.moveTo(x/xs+5*Math.abs(dx),y/ys+5*dy);
this.ctx.moveTo(this.cx / xs + space * Math.abs(this.dx), this.cy / ys + space * this.dy);
this.ctx.arc(this.cx / xs, this.cy / ys, space, Math.PI / 2 * this.dy, Math.PI + Math.PI / 2 * this.dy, 1);
this.cx += space * this.dx;
this.cy += space * this.dy;
if (n != 0) {
if (this.dx >= 0) {
this.ctx.moveTo(this.cx / xs - space * this.dx, this.cy / ys - space * this.dy);
}
this.ctx.moveTo(this.cx / xs - space * this.dx, this.cy / ys - space * this.dy);
this.ctx.arc(this.cx / xs - space / 2 * this.dx, this.cy / ys - space / 2 * this.dy, 1.5, Math.PI + Math.PI / 2 * this.dy, Math.PI / 2 * this.dy, 1);
}
}
this.ctx.moveTo(this.cx / xs - 1.75 * this.dx, this.cy / ys - 1.75 * this.dy);
this.ctx.scale(1 / xs, 1 / ys);
this.ctx.lineTo(this.cx, this.cy);
this.drawWire(len);
}
trimmer(len=50, label="") {
//capacitor
var space=5;
var hh = space * 1.8;
var cl = (len-space)/2;
var size=1.4*hh;
var psize = size*Math.cos(Math.PI/4);
this.newelement(len);
//draw capacitor
this.ctx.moveTo(0,0);
this.ctx.lineTo(cl,0);
this.ctx.fillText(label, cl, hh+10)
this.ctx.moveTo(cl, -hh);
this.ctx.lineTo(cl, hh);
this.ctx.moveTo(cl+space, -hh);
this.ctx.lineTo(cl+space, hh);
this.ctx.moveTo(cl+space, 0);
this.ctx.lineTo(len,0);
var x = len/2-psize;
var y = 0+psize;
this.ctx.moveTo(x,y);
var x1 = len/2+psize;
var y1 = 0-psize;
this.ctx.lineTo(x1,y1);
//short line
psize /= 3;
x = x1-psize;
y = y1-psize;
this.ctx.moveTo(x,y);
x = x1+psize;
y = y1+psize;
this.ctx.lineTo(x,y);
this.endelement(len);
}
drawTrimmer(len=50, label="") {
var size=12;
var psize = size*Math.cos(Math.PI/4);
var x = this.cx+len/2*this.dx;
var y = this.cy+len/2*this.dy;
var x1 = x-psize*Math.abs(this.dx-this.dy);
var y1 = y+psize*Math.abs(this.dy-this.dx);
this.ctx.moveTo(x1,y1);
x1 = x+psize*Math.abs(this.dx-this.dy);
y1 = y-psize*Math.abs(this.dy-this.dx);
this.ctx.lineTo(x1,y1);
//short line
psize /= 3;
x = x1-psize*Math.abs(this.dx-this.dy);
y = y1-psize*Math.abs(this.dy-this.dx);
this.ctx.moveTo(x,y);
x = x1+psize*Math.abs(this.dx-this.dy);
y = y1+psize*Math.abs(this.dy-this.dx);
this.ctx.lineTo(x,y);
this.ctx.moveTo(this.cx, this.cy);
this.drawCapacitor(len, label);
}
resistor(len=50, n=5, style=1, label="") {
var size = 5;
var wl = (len-(n+1)*size)/2;
this.newelement(len);
this.ctx.lineTo(wl,0);
this.ctx.fillText(label, wl, size+15);
if (style == 1) {
var x = wl+size;
var y = -size;
while (n--) {
this.ctx.lineTo(x,y);
this.ctx.lineTo(x,y+2*size);
x += size;
}
this.ctx.lineTo(len-wl, 0);
} else {
this.ctx.rect(wl,-size, size*(n+1), 2*size);
}
this.ctx.moveTo(len-wl, 0);
this.ctx.lineTo(len,0);
this.endelement(len);
}
drawResistor(len=50, n=5, style=1, label="") {
var size = 5;
var wl = (len-(n+1)*size)/2;
this.drawWire(wl);
this.ctx.fillText(label, this.cx+this.dy*(size+15), this.cy+this.dx*(size+15));
if (style == 1) {
this.cx += this.dx * size;
this.cy += this.dy * size;
while (n--) {
this.ctx.lineTo(this.cx - size * this.dy, this.cy - size * this.dx);
this.ctx.lineTo(this.cx + size * this.dy, this.cy + size * this.dx);
this.cx += size * this.dx;
this.cy += size * this.dy;
}
this.ctx.lineTo(this.cx, this.cy);
} else {
this.ctx.rect(this.cx-size*this.dy, this.cy-size*this.dx, size*(n+1)*this.dx+2*size*this.dy, size*(n+1)*this.dy+2*size*this.dx);
this.cx += this.dx * size*(n+1);
this.cy += this.dy * size*(n+1) ;
this.ctx.moveTo(this.cx, this.cy);
}
this.drawWire(wl);
}
drawSwitch(len=50, open=true, label="S") {
var size=len/2;
var wl = (len-size)/2;
this.drawWire(wl);
var x = this.cx;
var y = this.cy;
this.ctx.fillText(label, x - 15*this.dy, y+15*this.dx);
//this.ctx.arc(x,y,2, 0, Math.PI*2);
x += size*this.dx;
y += size*this.dy;
if (open) {
this.ctx.lineTo(x-size/2*this.dy, y-size/2*this.dx);
} else {
this.ctx.lineTo(x, y);
}
this.ctx.arc(this.cx, this.cy, 2, 0, Math.PI*2);
this.ctx.moveTo(x, y);
this.ctx.arc(x,y, 2,0, Math.PI*2);
this.ctx.moveTo(x, y);
this.cx = x;
this.cy = y;
this.drawWire(wl);
}
switch(len=50, open=true, label="S") {
var size=len/2;
var circle = 2;
var wl = (len-size)/2;
this.newelement(len);
this.ctx.lineTo(wl,0);
this.ctx.moveTo(wl+circle,0);
this.ctx.fillText(label, wl+circle, circle+15);
this.ctx.arc(wl+circle,0, circle, 0, Math.PI*2);
this.ctx.moveTo(wl+size-circle, 0);
this.ctx.arc(wl+size-circle,0,circle, 0, Math.PI*2);
this.ctx.moveTo(wl+circle,-circle);
if (open) {
this.ctx.lineTo(wl+size, -circle-size/2);
} else {
this.ctx.lineTo(wl+size, -circle);
}
this.ctx.moveTo(len-wl,0);
this.ctx.lineTo(len,0);
this.endelement(len);
}
turnClockwise() {
this.d++;
this.dx = Math.cos(Math.PI/2 * this.d);
this.dy = Math.sin(Math.PI/2 * this.d);
}
turnCounterClockwise() {
this.d--;
this.dx = Math.cos(Math.PI/2 * this.d);
this.dy = Math.sin(Math.PI/2 * this.d);
}
}
var cc = new Circuit("canvas",100, 100);
cc.ctx.lineWidth = 2;
cc.start(100, 300);
cc.wire();
cc.power(50, 2, "E");
cc.resistor(50,4,1,"R");
cc.switch(50,false)
//cc.wire();
//cc.turnCounterClockwise();
cc.capacitor(50,"C");
cc.trimmer(50,"T");
cc.wire();
cc.turnClockwise();
//cc.wire();
cc.trimmer(50,"T");
cc.inductor(50, 4, "Inductor");
cc.wire();
cc.turnClockwise();
//cc.wire();
cc.capacitor(50,"CC");
//cc.trimmer(50,"T2");
cc.inductor(50,5,"Ind");
//cc.wire();
cc.resistor(50,4,2,"R");
cc.switch();
cc.save();
cc.turnCounterClockwise();
cc.wire(20);
cc.turnClockwise();
cc.resistor(50,6,1,"R6");
cc.turnClockwise();
cc.wire(20);
cc.restore();
cc.turnClockwise();
cc.wire(20);
cc.turnCounterClockwise();
cc.resistor(50,5,1,"R5");
cc.turnCounterClockwise();
cc.wire(20);
cc.turnClockwise();
cc.wire();
cc.resistor(50,4,1,"R");
cc.turnClockwise();
cc.wire();
//cc.drawSwitch(50,false, "S3");
cc.power(50, 1, "E2");
cc.finish();
cc.start(100,250);
cc.wire();
cc.capacitor();
cc.turnClockwise();
cc.wire(20);
cc.turnCounterClockwise();
cc.capacitor();
cc.turnCounterClockwise();
cc.wire(20);
cc.save();
cc.wire(20);
cc.turnCounterClockwise();
cc.capacitor();
cc.turnCounterClockwise();
cc.wire(20);
cc.restore();
cc.turnClockwise();
cc.wire();
cc.turnCounterClockwise();
cc.wire(20);
cc.switch();
cc.wire(20);
cc.turnCounterClockwise();
cc.resistor();
cc.wire();
cc.trimmer();
cc.inductor();
cc.turnCounterClockwise();
cc.power();
cc.finish();
<canvas style="position:fixed;top:0;left:0;border:1px;" width="500" height="500" id="canvas">
Create a registry for your switches. It stores bounding boxes and states.
Calculate the position and size of each switch upon its creation (so that you can get x1,y1 & x2,y2, for a bounding box).
Enter each switch into the registry upon its creation via a "register" method.
Create a Click or MouseUp handler for the canvas. On Click or MouseUp, report the position of the mouse to the registry, via a "handleClick" method.
If the mouse location reported to the registry is within the bounding box of a switch, toggle that switch (both graphically and functionally).
Something like this:
class SwitchRegister {
var x1, x2, y1, y2; // float
var state; // boolean
constructor(x1, y1, x2, y2, state) {
this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2;
this.state = new Boolean(state);
}
}
class SwitchRegistry {
var registers; // array of SwitchRegister
register(x1, y1, x2, y2, state) {
this.registers[this.registers.length] = new SwitchRegister(x1, y1, x2, y2, state);
}
handleClick(x, y) {
for (i = 0; i < this.registers.length; i++) {
if (x >= this.registers[i].x1 && x <= this.registers[i].x2
&& y >= this.registers[i].y1 && y <= this.registers[i].y2) {
this.registers[i].state = !this.registers[i].state; // toggle state
// redraw the switch
// toggle its function within the sim
}
}
}
}

How to change color of circle when collision detection occurs?

As far as I understood it seems that even after changing the color when the collision is detected it reverts back to blue due to the else statement when it is compared between other circle and they are not colliding. So how would you solve this so that that the instance when the collision between any circle occurs it changes to red
collision detection
this.update = function() {
for (let i = 0; i < circles.length; i++) {
if (this !== circles[i] && getDistance(this.x, this.y, circles[i].x, circles[i].y) <= 200 * 200) {
this.c = 'red';
circles[i].c = 'red';
resolveCollision(this, circles[i]);
} else {
this.c = 'blue';
circles[i].c = 'blue';
}
}
//wall deflection
if (this.x - this.r <= 0 || this.x + this.r >= innerWidth)
this.v.x *= -1
if (this.y - this.r <= 0 || this.y + this.r >= innerHeight)
this.v.y *= -1
this.x += this.v.x;
this.y += this.v.y;
this.draw();
};
//deflection amongst other circles
function resolveCollision(circle, othercircle) {
const xVelocityDiff = circle.v.x - othercircle.v.x;
const yVelocityDiff = circle.v.y - othercircle.v.y;
const xDist = othercircle.x - circle.x;
const yDist = othercircle.y - circle.y;
if (xVelocityDiff * xDist + yVelocityDiff * yDist >= 0) {
const angle = -Math.atan2(othercircle.y - circle.y, othercircle.x - circle.x);
const m1 = circle.m;
const m2 = othercircle.m;
const u1 = rotate(circle.v, angle);
const u2 = rotate(othercircle.v, angle);
const v1 = {
x: u1.x * (m1 - m2) / (m1 + m2) + u2.x * 2 * m2 / (m1 + m2),
y: u1.y
}
const v2 = {
x: u2.x * (m1 - m2) / (m1 + m2) + u1.x * 2 * m2 / (m1 + m2),
y: u2.y
}
const vFinal1 = rotate(v1, -angle);
const vFinal2 = rotate(v2, -angle);
circle.v.x = vFinal1.x;
circle.v.y = vFinal1.y;
othercircle.v.x = vFinal2.x;
othercircle.v.y = vFinal2.y;
}
}
Semaphores
Use a semaphore that holds the collision state of the circle.
Thus in your Circle.prototype would have something like these functions and properties
Circle.prototype = {
collided: false, // when true change color
draw() {
ctx.strokeStyle = this.collided ? "red" : "blue";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - 1.5, 0, Math.PI * 2);
ctx.stroke();
},
...
...
// in update
update() {
// when collision is detected set semaphore
if (collision) {
this.collided = true;
}
}
}
Counters
Or you may want to only have the color change last for some time. You can modify the semaphore and use it as a counter. On collision set it to the number of frames to change color for.
Circle.prototype = {
collided: 0,
draw() {
ctx.strokeStyle = this.collided ? (this.collided--, "red") : "blue";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - 1.5, 0, Math.PI * 2);
ctx.stroke();
},
...
...
// in update
update() {
// when collision is detected set semaphore
if (collision) {
this.collided = 60; // 1 second at 60FPS
}
}
}
Example
This example is taken from another answer I did earlier this year.
As there is a lot of code I have highlighted the relevant code with
/*= ANSWER CODE ==============================================================
...
=============================================================================*/
The example uses counters and changes color for 30 frames after a collision with another ball or wall.
I did not use a semaphore as all the balls would be red within a second.
canvas.width = innerWidth -20;
canvas.height = innerHeight -20;
mathExt(); // creates some additional math functions
const ctx = canvas.getContext("2d");
const GRAVITY = 0;
const WALL_LOSS = 1;
const BALL_COUNT = 10; // approx as will not add ball if space can not be found
const MIN_BALL_SIZE = 6;
const MAX_BALL_SIZE = 30;
const VEL_MIN = 1;
const VEL_MAX = 5;
const MAX_RESOLUTION_CYCLES = 100; // Put too many balls (or too large) in the scene and the
// number of collisions per frame can grow so large that
// it could block the page.
// If the number of resolution steps is above this value
// simulation will break and balls can pass through lines,
// get trapped, or worse. LOL
const SHOW_COLLISION_TIME = 30;
const balls = [];
const lines = [];
function Line(x1,y1,x2,y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
Line.prototype = {
draw() {
ctx.moveTo(this.x1, this.y1);
ctx.lineTo(this.x2, this.y2);
},
reverse() {
const x = this.x1;
const y = this.y1;
this.x1 = this.x2;
this.y1 = this.y2;
this.x2 = x;
this.y2 = y;
return this;
}
}
function Ball(x, y, vx, vy, r = 45, m = 4 / 3 * Math.PI * (r ** 3)) {
this.r = r;
this.m = m
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
/*= ANSWER CODE ==============================================================*/
this.collided = 0;
/*============================================================================*/
}
Ball.prototype = {
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += GRAVITY;
},
draw() {
/*= ANSWER CODE ==============================================================*/
ctx.strokeStyle = this.collided ? (this.collided--, "#F00") : "#00F";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - 1.5, 0, Math.PI * 2);
ctx.stroke();
/* ============================================================================*/
},
interceptLineTime(l, time) {
const u = Math.interceptLineBallTime(this.x, this.y, this.vx, this.vy, l.x1, l.y1, l.x2, l.y2, this.r);
if (u >= time && u <= 1) {
return u;
}
},
checkBallBallTime(t, minTime) {
return t > minTime && t <= 1;
},
interceptBallTime(b, time) {
const x = this.x - b.x;
const y = this.y - b.y;
const d = (x * x + y * y) ** 0.5;
if (d > this.r + b.r) {
const times = Math.circlesInterceptUnitTime(
this.x, this.y,
this.x + this.vx, this.y + this.vy,
b.x, b.y,
b.x + b.vx, b.y + b.vy,
this.r, b.r
);
if (times.length) {
if (times.length === 1) {
if(this.checkBallBallTime(times[0], time)) { return times[0] }
return;
}
if (times[0] <= times[1]) {
if(this.checkBallBallTime(times[0], time)) { return times[0] }
if(this.checkBallBallTime(times[1], time)) { return times[1] }
return
}
if(this.checkBallBallTime(times[1], time)) { return times[1] }
if(this.checkBallBallTime(times[0], time)) { return times[0] }
}
}
},
collideLine(l, time) {
/*= ANSWER CODE ==============================================================*/
this.collided = SHOW_COLLISION_TIME;
/*============================================================================*/
const x1 = l.x2 - l.x1;
const y1 = l.y2 - l.y1;
const d = (x1 * x1 + y1 * y1) ** 0.5;
const nx = x1 / d;
const ny = y1 / d;
const u = (this.vx * nx + this.vy * ny) * 2;
this.x += this.vx * time;
this.y += this.vy * time;
this.vx = (nx * u - this.vx) * WALL_LOSS;
this.vy = (ny * u - this.vy) * WALL_LOSS;
this.x -= this.vx * time;
this.y -= this.vy * time;
},
collide(b, time) { // b is second ball
/*= ANSWER CODE ==============================================================*/
this.collided = SHOW_COLLISION_TIME;
b.collided = SHOW_COLLISION_TIME;
/*============================================================================*/
const a = this;
const m1 = a.m;
const m2 = b.m;
a.x = a.x + a.vx * time;
a.y = a.y + a.vy * time;
b.x = b.x + b.vx * time;
b.y = b.y + b.vy * time;
const x = a.x - b.x
const y = a.y - b.y
const d = (x * x + y * y);
const u1 = (a.vx * x + a.vy * y) / d
const u2 = (x * a.vy - y * a.vx ) / d
const u3 = (b.vx * x + b.vy * y) / d
const u4 = (x * b.vy - y * b.vx ) / d
const mm = m1 + m2;
const vu3 = (m1 - m2) / mm * u1 + (2 * m2) / mm * u3;
const vu1 = (m2 - m1) / mm * u3 + (2 * m1) / mm * u1;
b.vx = x * vu1 - y * u4;
b.vy = y * vu1 + x * u4;
a.vx = x * vu3 - y * u2;
a.vy = y * vu3 + x * u2;
a.x = a.x - a.vx * time;
a.y = a.y - a.vy * time;
b.x = b.x - b.vx * time;
b.y = b.y - b.vy * time;
},
doesOverlap(ball) {
const x = this.x - ball.x;
const y = this.y - ball.y;
return (this.r + ball.r) > ((x * x + y * y) ** 0.5);
}
}
function canAdd(ball) {
for(const b of balls) {
if (ball.doesOverlap(b)) { return false }
}
return true;
}
function create(bCount) {
lines.push(new Line(-10, 20, ctx.canvas.width + 10, 5));
lines.push((new Line(-10, ctx.canvas.height - 2, ctx.canvas.width + 10, ctx.canvas.height - 30)).reverse());
lines.push((new Line(30, -10, 4, ctx.canvas.height + 10)).reverse());
lines.push(new Line(ctx.canvas.width - 3, -10, ctx.canvas.width - 30, ctx.canvas.height + 10));
while (bCount--) {
let tries = 100;
while (tries--) {
const dir = Math.rand(0, Math.TAU);
const vel = Math.rand(VEL_MIN, VEL_MAX);
const ball = new Ball(
Math.rand(MAX_BALL_SIZE + 30, canvas.width - MAX_BALL_SIZE - 30),
Math.rand(MAX_BALL_SIZE + 30, canvas.height - MAX_BALL_SIZE - 30),
Math.cos(dir) * vel,
Math.sin(dir) * vel,
Math.rand(MIN_BALL_SIZE, MAX_BALL_SIZE),
);
if (canAdd(ball)) {
balls.push(ball);
break;
}
}
}
}
function resolveCollisions() {
var minTime = 0, minObj, minBall, resolving = true, idx = 0, idx1, after = 0, e = 0;
while (resolving && e++ < MAX_RESOLUTION_CYCLES) { // too main ball may create very lone resolution cycle. e limits this
resolving = false;
minObj = undefined;
minBall = undefined;
minTime = 1;
idx = 0;
for (const b of balls) {
idx1 = idx + 1;
while (idx1 < balls.length) {
const b1 = balls[idx1++];
const time = b.interceptBallTime(b1, after);
if (time !== undefined) {
if (time <= minTime) {
minTime = time;
minObj = b1;
minBall = b;
resolving = true;
}
}
}
for (const l of lines) {
const time = b.interceptLineTime(l, after);
if (time !== undefined) {
if (time <= minTime) {
minTime = time;
minObj = l;
minBall = b;
resolving = true;
}
}
}
idx ++;
}
if (resolving) {
if (minObj instanceof Ball) {
minBall.collide(minObj, minTime);
} else {
minBall.collideLine(minObj, minTime);
}
after = minTime;
}
}
}
create(BALL_COUNT);
mainLoop();
function mainLoop() {
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
resolveCollisions();
for (const b of balls) { b.update() }
for (const b of balls) { b.draw() }
ctx.lineWidth = 1;
ctx.strokeStyle = "#000";
ctx.beginPath();
for(const l of lines) { l.draw() }
ctx.stroke();
requestAnimationFrame(mainLoop);
}
function mathExt() {
Math.TAU = Math.PI * 2;
Math.rand = (min, max) => Math.random() * (max - min) + min;
Math.randI = (min, max) => Math.random() * (max - min) + min | 0; // only for positive numbers 32bit signed int
Math.randItem = arr => arr[Math.random() * arr.length | 0]; // only for arrays with length < 2 ** 31 - 1
// contact points of two circles radius r1, r2 moving along two lines (a,e)-(b,f) and (c,g)-(d,h) [where (,) is coord (x,y)]
Math.circlesInterceptUnitTime = (a, e, b, f, c, g, d, h, r1, r2) => { // args (x1, y1, x2, y2, x3, y3, x4, y4, r1, r2)
const A = a * a, B = b * b, C = c * c, D = d * d;
const E = e * e, F = f * f, G = g * g, H = h * h;
var R = (r1 + r2) ** 2;
const AA = A + B + C + F + G + H + D + E + b * c + c * b + f * g + g * f + 2 * (a * d - a * b - a * c - b * d - c * d - e * f + e * h - e * g - f * h - g * h);
const BB = 2 * (-A + a * b + 2 * a * c - a * d - c * b - C + c * d - E + e * f + 2 * e * g - e * h - g * f - G + g * h);
const CC = A - 2 * a * c + C + E - 2 * e * g + G - R;
return Math.quadRoots(AA, BB, CC);
}
Math.quadRoots = (a, b, c) => { // find roots for quadratic
if (Math.abs(a) < 1e-6) { return b != 0 ? [-c / b] : [] }
b /= a;
var d = b * b - 4 * (c / a);
if (d > 0) {
d = d ** 0.5;
return [0.5 * (-b + d), 0.5 * (-b - d)]
}
return d === 0 ? [0.5 * -b] : [];
}
Math.interceptLineBallTime = (x, y, vx, vy, x1, y1, x2, y2, r) => {
const xx = x2 - x1;
const yy = y2 - y1;
const d = vx * yy - vy * xx;
if (d > 0) { // only if moving towards the line
const dd = r / (xx * xx + yy * yy) ** 0.5;
const nx = xx * dd;
const ny = yy * dd;
return (xx * (y - (y1 + nx)) - yy * (x -(x1 - ny))) / d;
}
}
}
<canvas id="canvas"></canvas>
const collided = {
color: 'red',
get current() {
return this.color
},
set current(clr) {
if (this.color === 'red') {
this.color = 'blue'
} else {
this.color = 'red'
}
}
}
this.update= function(){
for(let i=0;i<circles.length;i++){
if(this!==circles[i] && getDistance(this.x,this.y,circles[i].x,circles[i].y)<=200*200){
this.c=collided.current
collided.current = this.c
circles[i].c=collided.current
resolveCollision(this,circles[i]);
}
// ...
}
}
The trick is to use use getters and setters to ensure that the most recently used color value is never reapplied
First, split the if:
// psudo-code
if this is not circles[i], then
if overlapping, then
do something
else
do something else
else
do nothing
Then, fix if this is not circles[i]:
{x:100,y:100}!={x:100,y:100}, so
either add id to circles and compare ids (my recommendation), or,
compare .xs and .ys (less desired - what if they are equal but not same circle?), or,
use JSON.stringify(a)==JSON.stringify(b).
I would add .overlapping and before the loop, I'd add another loop setting all .overlapping to false, then, withing the modified original loop, I'd check if .overlapping is false, and then if there is a collision, I'd set .overlapping to true for both.
Another way would be to create an array to hole overlapping circles' .ids and check if that array includes the current loop item's .id.

Balls bouncing off of each other

I am working on this script where I have x-number bouncing balls (in this case 20 balls) in a canvas.
My question is, how do I make them bounce off each other when they hit, as well as bounce off the yellow rectangle when they hit it?
var mycanvas =document.getElementById("mycanvas");
var ctx=mycanvas.getContext("2d");
var w=500,h=500;
mycanvas.height=h;
mycanvas.width=w;
var ball=[];
function Ball(x,y,r,c,vx,vy){
this.x=x; //starting x coordinate
this.y=y; //starting x coordinate
this.r=r; //radius
this.c=c; //color
this.vx=vx; // x direction speed
this.vy=vy; // y direction speed
this.update=function(){
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI*2, false);
ctx.fillStyle = this.c;
ctx.fill();
ctx.closePath();
this.x += this.vx;
this.y += this.vy;
//changing direction on hitting wall
if(this.y>=(w-10)||this.y<=10){
this.vy=-this.vy;
}
if(this.x>=(h-10)||this.x<=10){
this.vx=-this.vx;
}
}
}
function clearCanvas(){
ctx.clearRect(0, 0, w, h);
}
var count;
for (count = 0; count < 20; count++) {
var rndColor=Math.floor((Math.random() * 9) + 1); //random color
ball[count]= new Ball(Math.floor((Math.random() * 490) + 1),Math.floor((Math.random() * 490)+1),5,'red',5,5);
}
function update(){
var i;
clearCanvas();
//draw rectangle
ctx.rect(250, 200, 10, 100);
ctx.fillStyle = 'yellow';
ctx.fill();
for(i=0;i<count;i++){
ball[i].update();
}
}
setInterval(update, 1000/60);
There are several methods you can use. The following methods are about the simplest.
Update
I have added an example that uses the second method. See snippet at the bottom.
Defining the balls
Each example is as an object called Ball.
// x,y position of center,
// vx,vy is velocity,
// r is radius defaults 45,
// m is mass defaults to the volume of the sphere of radius r
function Ball(x, y, vx, vy, r = 45, m = (4 / 3 * Math.PI * (r ** 3)) {
this.r = r;
this.m = m;
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
}
Ball.prototype = {
// add collision functions here
};
The code assumes the balls are touching.
Elastic collisions
The logic used can be found at wikis elastic collision page
The calculation splits the forces into two parts for each ball. (4 in total for 2 balls)
The transfer of energy along the line between the balls,
The adjustment of energy per ball along the tangent of the collision point
Equal mass
Each ball has the same mass which means that the transfer of energy is balanced and can be ignored
After the function is called each ball has a new velocity vector.
Note that if you call collision and the velocities mean that the balls are moving away from each other (collision paradox) then the result will have the balls moving into each other (resolution paradox)
To keep the math simple the vector magnitudes u1, u2, u3, and u4 are converted into a unit that is the length of the line between the ball centers (square root of d)
collide(b) { // b is the ball that the collision is with
const a = this;
const x = a.x - b.x;
const y = a.y - b.y;
const d = x * x + y * y;
const u1 = (a.vx * x + a.vy * y) / d; // From this to b
const u2 = (x * a.vy - y * a.vx) / d; // Adjust self along tangent
const u3 = (b.vx * x + b.vy * y) / d; // From b to this
const u4 = (x * b.vy - y * b.vx) / d; // Adjust b along tangent
// set new velocities
b.vx = x * u1 - y * u4;
b.vy = y * u1 + x * u4;
a.vx = x * u3 - y * u2;
a.vy = y * u3 + x * u2;
},
Different masses
Each ball has its own mass and thus the transfer needs to calculate the amount of energy related to the mass that is transferred.
Only the energy transferred along the line between the balls is effect by the mass differences
collideMass(b) {
const a = this;
const m1 = a.m;
const m2 = b.m;
const x = a.x - b.x;
const y = a.y - b.y;
const d = x * x + y * y;
const u1 = (a.vx * x + a.vy * y) / d;
const u2 = (x * a.vy - y * a.vx) / d;
const u3 = (b.vx * x + b.vy * y) / d;
const u4 = (x * b.vy - y * b.vx) / d;
const mm = m1 + m2;
const vu3 = (m1 - m2) / mm * u1 + (2 * m2) / mm * u3;
const vu1 = (m2 - m1) / mm * u3 + (2 * m1) / mm * u1;
b.vx = x * vu1 - y * u4;
b.vy = y * vu1 + x * u4;
a.vx = x * vu3 - y * u2;
a.vy = y * vu3 + x * u2;
},
Example
Simple ball collision example. Balls bound by lines (Note lines have an outside and inside, if looking from the start to the end the inside is on the right)
Collisions are fully resolved in chronological order between frames. The time used is a frame where 0 is the previous frame and 1 is the current frame.
canvas.width = innerWidth -20;
canvas.height = innerHeight -20;
const ctx = canvas.getContext("2d");
const GRAVITY = 0;
const WALL_LOSS = 1;
const BALL_COUNT = 20; // approx as will not add ball if space can not be found
const MIN_BALL_SIZE = 13;
const MAX_BALL_SIZE = 20;
const VEL_MIN = 1;
const VEL_MAX = 5;
const MAX_RESOLUTION_CYCLES = 100;
Math.TAU = Math.PI * 2;
Math.rand = (min, max) => Math.random() * (max - min) + min;
Math.randI = (min, max) => Math.random() * (max - min) + min | 0; // only for positive numbers 32bit signed int
Math.randItem = arr => arr[Math.random() * arr.length | 0]; // only for arrays with length < 2 ** 31 - 1
// contact points of two circles radius r1, r2 moving along two lines (a,e)-(b,f) and (c,g)-(d,h) [where (,) is coord (x,y)]
Math.circlesInterceptUnitTime = (a, e, b, f, c, g, d, h, r1, r2) => { // args (x1, y1, x2, y2, x3, y3, x4, y4, r1, r2)
const A = a * a, B = b * b, C = c * c, D = d * d;
const E = e * e, F = f * f, G = g * g, H = h * h;
var R = (r1 + r2) ** 2;
const AA = A + B + C + F + G + H + D + E + b * c + c * b + f * g + g * f + 2 * (a * d - a * b - a * c - b * d - c * d - e * f + e * h - e * g - f * h - g * h);
const BB = 2 * (-A + a * b + 2 * a * c - a * d - c * b - C + c * d - E + e * f + 2 * e * g - e * h - g * f - G + g * h);
const CC = A - 2 * a * c + C + E - 2 * e * g + G - R;
return Math.quadRoots(AA, BB, CC);
}
Math.quadRoots = (a, b, c) => { // find roots for quadratic
if (Math.abs(a) < 1e-6) { return b != 0 ? [-c / b] : [] }
b /= a;
var d = b * b - 4 * (c / a);
if (d > 0) {
d = d ** 0.5;
return [0.5 * (-b + d), 0.5 * (-b - d)]
}
return d === 0 ? [0.5 * -b] : [];
}
Math.interceptLineBallTime = (x, y, vx, vy, x1, y1, x2, y2, r) => {
const xx = x2 - x1;
const yy = y2 - y1;
const d = vx * yy - vy * xx;
if (d > 0) { // only if moving towards the line
const dd = r / (xx * xx + yy * yy) ** 0.5;
const nx = xx * dd;
const ny = yy * dd;
return (xx * (y - (y1 + nx)) - yy * (x -(x1 - ny))) / d;
}
}
const balls = [];
const lines = [];
function Line(x1,y1,x2,y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
Line.prototype = {
draw() {
ctx.moveTo(this.x1, this.y1);
ctx.lineTo(this.x2, this.y2);
},
reverse() {
const x = this.x1;
const y = this.y1;
this.x1 = this.x2;
this.y1 = this.y2;
this.x2 = x;
this.y2 = y;
return this;
}
}
function Ball(x, y, vx, vy, r = 45, m = 4 / 3 * Math.PI * (r ** 3)) {
this.r = r;
this.m = m
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
}
Ball.prototype = {
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += GRAVITY;
},
draw() {
ctx.moveTo(this.x + this.r, this.y);
ctx.arc(this.x, this.y, this.r, 0, Math.TAU);
},
interceptLineTime(l, time) {
const u = Math.interceptLineBallTime(this.x, this.y, this.vx, this.vy, l.x1, l.y1, l.x2, l.y2, this.r);
if(u >= time && u <= 1) {
return u;
}
},
checkBallBallTime(t, minTime) {
return t > minTime && t <= 1;
},
interceptBallTime(b, time) {
const x = this.x - b.x;
const y = this.y - b.y;
const d = (x * x + y * y) ** 0.5;
if(d > this.r + b.r) {
const times = Math.circlesInterceptUnitTime(
this.x, this.y,
this.x + this.vx, this.y + this.vy,
b.x, b.y,
b.x + b.vx, b.y + b.vy,
this.r, b.r
);
if(times.length) {
if(times.length === 1) {
if(this.checkBallBallTime(times[0], time)) { return times[0] }
return;
}
if(times[0] <= times[1]) {
if(this.checkBallBallTime(times[0], time)) { return times[0] }
if(this.checkBallBallTime(times[1], time)) { return times[1] }
return
}
if(this.checkBallBallTime(times[1], time)) { return times[1] }
if(this.checkBallBallTime(times[0], time)) { return times[0] }
}
}
},
collideLine(l, time) {
const x1 = l.x2 - l.x1;
const y1 = l.y2 - l.y1;
const d = (x1 * x1 + y1 * y1) ** 0.5;
const nx = x1 / d;
const ny = y1 / d;
const u = (this.vx * nx + this.vy * ny) * 2;
this.x += this.vx * time;
this.y += this.vy * time;
this.vx = (nx * u - this.vx) * WALL_LOSS;
this.vy = (ny * u - this.vy) * WALL_LOSS;
this.x -= this.vx * time;
this.y -= this.vy * time;
},
collide(b, time) {
const a = this;
const m1 = a.m;
const m2 = b.m;
const x = a.x - b.x
const y = a.y - b.y
const d = (x * x + y * y);
const u1 = (a.vx * x + a.vy * y) / d
const u2 = (x * a.vy - y * a.vx ) / d
const u3 = (b.vx * x + b.vy * y) / d
const u4 = (x * b.vy - y * b.vx ) / d
const mm = m1 + m2;
const vu3 = (m1 - m2) / mm * u1 + (2 * m2) / mm * u3;
const vu1 = (m2 - m1) / mm * u3 + (2 * m1) / mm * u1;
a.x = a.x + a.vx * time;
a.y = a.y + a.vy * time;
b.x = b.x + b.vx * time;
b.y = b.y + b.vy * time;
b.vx = x * vu1 - y * u4;
b.vy = y * vu1 + x * u4;
a.vx = x * vu3 - y * u2;
a.vy = y * vu3 + x * u2;
a.x = a.x - a.vx * time;
a.y = a.y - a.vy * time;
b.x = b.x - b.vx * time;
b.y = b.y - b.vy * time;
},
doesOverlap(ball) {
const x = this.x - ball.x;
const y = this.y - ball.y;
return (this.r + ball.r) > ((x * x + y * y) ** 0.5);
}
}
function canAdd(ball) {
for(const b of balls) {
if (ball.doesOverlap(b)) { return false }
}
return true;
}
function create(bCount) {
lines.push(new Line(-10, 10, ctx.canvas.width + 10, 5));
lines.push((new Line(-10, ctx.canvas.height - 2, ctx.canvas.width + 10, ctx.canvas.height - 10)).reverse());
lines.push((new Line(10, -10, 4, ctx.canvas.height + 10)).reverse());
lines.push(new Line(ctx.canvas.width - 3, -10, ctx.canvas.width - 10, ctx.canvas.height + 10));
while (bCount--) {
let tries = 100;
debugger
while (tries--) {
const dir = Math.rand(0, Math.TAU);
const vel = Math.rand(VEL_MIN, VEL_MAX);
const ball = new Ball(
Math.rand(MAX_BALL_SIZE + 10, canvas.width - MAX_BALL_SIZE - 10),
Math.rand(MAX_BALL_SIZE + 10, canvas.height - MAX_BALL_SIZE - 10),
Math.cos(dir) * vel,
Math.sin(dir) * vel,
Math.rand(MIN_BALL_SIZE, MAX_BALL_SIZE),
);
if (canAdd(ball)) {
balls.push(ball);
break;
}
}
}
}
function resolveCollisions() {
var minTime = 0, minObj, minBall, resolving = true, idx = 0, idx1, after = 0, e = 0;
while(resolving && e++ < MAX_RESOLUTION_CYCLES) { // too main ball may create very lone resolution cycle. e limits this
resolving = false;
minObj = undefined;
minBall = undefined;
minTime = 1;
idx = 0;
for(const b of balls) {
idx1 = idx + 1;
while(idx1 < balls.length) {
const b1 = balls[idx1++];
const time = b.interceptBallTime(b1, after);
if(time !== undefined) {
if(time <= minTime) {
minTime = time;
minObj = b1;
minBall = b;
resolving = true;
}
}
}
for(const l of lines) {
const time = b.interceptLineTime(l, after);
if(time !== undefined) {
if(time <= minTime) {
minTime = time;
minObj = l;
minBall = b;
resolving = true;
}
}
}
idx ++;
}
if(resolving) {
if (minObj instanceof Ball) {
minBall.collide(minObj, minTime);
} else {
minBall.collideLine(minObj, minTime);
}
after = minTime;
}
}
}
create(BALL_COUNT);
mainLoop();
function mainLoop() {
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
resolveCollisions();
for(const b of balls) { b.update() }
ctx.strokeStyle = "#000";
ctx.beginPath();
for(const b of balls) { b.draw() }
for(const l of lines) { l.draw() }
ctx.stroke();
requestAnimationFrame(mainLoop);
}
<canvas id="canvas"></canvas>
To bounce balls off of one another, he's what you need to know
Have the balls collided? The way to determine is to measure the distance between the centers of the two circles. If this is less than the combined radiuses, the balls have collided
What direction should they have after colliding? Use use atan2 to calculate the angle between the centers of the two balls. Then set them in opposite directions on that angle, in a way that they don't end up deeper within each other. Of course, this simple solution ignores existing momentum that the balls may have. But doing the momentum calculation (which involves mass, speed, and current angle) is more complicated.

Categories