I need to draw multiples balls inside a rect. I have a rect and 4 informations. Width and height of the rect.. numbers of balls per line and numbers of lines. That's been said I have to draw, for example, 4 balls at the same line. starting by the corners(That I was able to do) but I can't figure out how to draw more than 2 balls, example: If I have 3 balls, I need to draw 2 in the corners and 1 in the middle, if I have 4 balls... 2 in the corners and 2 in the middle. I had the idea of think about the rect as a matrix but having no luck.. link to see what I mean
If you need to drawn for example n dragon balls on line then you can divide length with n + 1 to get spacing between center of balls, or if you want different offset on start and end then you would divide (width - 2*offset) / (n - 1).
<canvas id="canvas" width="300" height="100">
</canvas>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
class Rect {
constructor(x, y, width, heght) {
this.x = x;
this.y = y;
this.width = width;
this.heght = heght;
}
}
class Circle {
constructor(x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
}
}
class Scene
{
constructor() {
this.items = [];
}
clear() {
this.items = [];
}
add(item) {
this.items.push(item);
}
draw(ctx) {
for(let item of this.items) {
if (item instanceof Rect) {
ctx.beginPath();
ctx.rect(item.x, item.y, item.width, item.heght);
ctx.stroke();
} else if (item instanceof Circle) {
ctx.beginPath();
ctx.arc(item.x, item.y, item.radius, 0, 2 * Math.PI);
ctx.stroke();
}
}
}
}
const scene = new Scene();
scene.clear();
scene.add(new Rect(0, 0, 300, 100));
let n = 5;
let offset = 30;
let spacing = ((300 - 2 * offset ) / (n - 1));
for (let i = 0; i < n; i++) {
scene.add(new Circle(i * spacing + offset, 50, 25))
}
scene.draw(ctx);
</script>
I loved it and i'm using it now... although i'm having trouble trying to positioning the balls inside my draw... see what I got so far and if you have a little more time to give me a hand in this <3 (I need to put the balls inside the third rect only no matter what width or height the user enter)
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height);
scene.clear();
context.beginPath();
context.strokeRect(zoomedX(0), zoomedY(0), zoomed(width), zoomed(height));
context.strokeRect(zoomedX(55), zoomedY(55), zoomed(width-10), zoomed(height-10));
context.strokeRect(zoomedX(60), zoomedY(60), zoomed(width-20), zoomed(height-20));
context.closePath();
let radius = 8;
let n = 3;
let lines = 3;
let offset = 68;
let offsetY = 68;
let spacing = ((width - 2 * offset ) / (n - 1));
let spacingY = ((height - 2 * offset ) / (lines - 1));
for (let i = 0; i < n; i++) {
for(let j = 0; j < lines ;j++){
scene.add(new Circle(i * spacing + offset, j * spacingY + offset, radius))
}
}
scene.draw(context);
}
Related
The Idea
I came across this idea of multiplication circles from a YouTube video that I stumbled upon and I thought that would be a fun thing to try and recreate using JavasSript and the canvas element.
The Original Video
The Problem
I smoothed out the animation the best I could but it still doesn't look as proper as I'd like. I suspect coming up with a solution would require a decent amount of math. To grasp the problem in detail I think it's easier to look at the code
window.onload = () => {
const app = document.querySelector('#app')
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const { cos, sin, PI } = Math
const Tao = PI * 2
const width = window.innerWidth
const height = window.innerHeight
const cx = width / 2
const cy = height / 2
const baseNumberingSystem = 200
const stop = 34
let color = 'teal'
let multiplier = 0
let drawQue = []
// setup canvas
canvas.width = width
canvas.height = height
class Circle {
constructor(x, y, r, strokeColor, fillColor) {
this.x = x
this.y = y
this.r = r
this.strokeColor = strokeColor || '#fff'
this.fillColor = fillColor || '#fff'
}
draw(stroke, fill) {
ctx.moveTo(this.x, this.y)
ctx.beginPath()
ctx.arc(this.x, this.y, this.r, 0, Tao)
ctx.closePath()
if (fill) {
ctx.fillStyle = this.fillColor
ctx.fill()
}
if (stroke) {
ctx.strokeStyle = this.strokeColor
ctx.stroke()
}
}
createChildCircleAt(degree, radius, strokeColor, fillColor) {
const radian = degreeToRadian(degree)
const x = this.x + (this.r * cos(radian))
const y = this.y + (this.r * sin(radian))
return new Circle(x, y, radius, strokeColor, fillColor)
}
divideCircle(nTimes, radius) {
const degree = 360 / nTimes
let division = 1;
while (division <= nTimes) {
drawQue.push(this.createChildCircleAt(division * degree, radius))
division++
}
}
}
function degreeToRadian(degree) {
return degree * (PI / 180)
}
function draw() {
const mainCircle = new Circle(cx, cy, cy * 0.9)
// empty the que
drawQue = []
// clear canvas
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = "black"
ctx.fillRect(0, 0, width, height)
// redraw everything
mainCircle.draw()
mainCircle.divideCircle(baseNumberingSystem, 4)
drawQue.forEach(item => item.draw())
// draw modular times table
for (let i = 1; i <= drawQue.length; i++) {
const product = i * multiplier;
const firstPoint = drawQue[i]
const secondPoint = drawQue[product % drawQue.length]
if (firstPoint && secondPoint) {
ctx.beginPath()
ctx.moveTo(firstPoint.x, firstPoint.y)
ctx.strokeStyle = color
ctx.lineTo(secondPoint.x, secondPoint.y)
ctx.closePath()
ctx.stroke()
}
}
}
function animate() {
multiplier+= 0.1
multiplier = parseFloat(multiplier.toFixed(2))
draw()
console.log(multiplier, stop)
if (multiplier === stop) {
clearInterval(animation)
}
}
app.appendChild(canvas)
let animation = setInterval(animate, 120)
}
So the main issue comes from when we increment the multiplier by values less than 1 in an attempt to make the animation more fluid feeling. Example: multiplier+= 0.1. When we do this it increase the amount of times our if block in our draw function will fail because the secondPoint will return null.
const product = i * multiplier; // this is sometimes a decimal
const firstPoint = drawQue[i]
const secondPoint = drawQue[product % drawQue.length] // therefore this will often not be found
// Then this if block wont execute. Which is good because if it did we the code would crash
// But I think what we need is an if clause to still draw a line to a value in between the two
// closest indices of our drawQue
if (firstPoint && secondPoint) {
//...
}
Possible Solution
I think what I'd need to do is when we fail to find the secondPoint get the remainder of product % drawQue.length and use that to create a new circle in between the two closest circles in the drawQue array and use that new circle as the second point of our line.
If you use requestAnimationFrame it looks smooth
function animate() {
if (multiplier != stop) {
multiplier+= 0.1
multiplier = parseFloat(multiplier.toFixed(2))
draw()
requestAnimationFrame(animate);
}
}
app.appendChild(canvas)
animate()
My possible solution ended up working. I'll leave the added else if block here for anyone whos interested. I also had to store the degree value in my circle objects when they were made as well as calculate the distance between each subdivision of the circle.
Added If Else Statement
for (let i = 1; i <= drawQue.length; i++) {
const product = i * multiplier;
const newIndex = product % drawQue.length
const firstPoint = drawQue[i]
const secondPoint = drawQue[newIndex]
if (firstPoint && secondPoint) {
ctx.beginPath()
ctx.moveTo(firstPoint.x, firstPoint.y)
ctx.strokeStyle = color
ctx.lineTo(secondPoint.x, secondPoint.y)
ctx.closePath()
ctx.stroke()
} else if (!secondPoint) {
const percent = newIndex % 1
const closest = drawQue[Math.floor(newIndex)]
const newDegree = closest.degree + (degreeIncriments * percent)
const target = mainCircle.createChildCircleAt(newDegree, 4)
if (firstPoint && target) {
ctx.beginPath()
ctx.moveTo(firstPoint.x, firstPoint.y)
ctx.strokeStyle = color
ctx.lineTo(target.x, target.y)
ctx.closePath()
ctx.stroke()
}
}
Other changes
// ...
const degreeIncriments = 360 / baseNumberingSystem
// ...
class Circle {
constructor(/* ... */, degree )
// ...
this.degree = degree || 0
}
Hope someone finds this useful...
I am trying to achieve a tracing effect where the lines have a faded trail. The way I am trying to do it is simply by drawing the solid background once, and then on further frames draw a transparent background before drawing the new lines, so that you can still see a little of the image before it.
The issue is that I do want the lines to fade out completely after some time, but they seem to leave a permanent after image, even after drawing over them repeatedly.
I've tried setting different globalCompositeOperation(s) and it seemed like I was barking up the wrong tree there.
This code is called once
//initiate trace bg
traceBuffer.getContext("2d").fillStyle = "rgba(0, 30, 50, 1)";
traceBuffer.getContext("2d").fillRect(0, 0, traceBuffer.width, traceBuffer.height);
then inside the setInterval function it calls
//draw transparent background
ctx.fillStyle = "rgba(0, 30, 50, 0.04)";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
//set drawing settings
ctx.strokeStyle = "#AAAAAA";
ctx.lineWidth = 4;
for (let i = 0; i < tracer.layer2.length; i++){
ctx.beginPath();
ctx.moveTo(newX, newY);
ctx.lineTo(oldX, oldY);
ctx.stroke();
}
Here's an example: https://i.imgur.com/QTkeIVf.png
On the left is what I am currently getting, and on the right is the edit of what I actually want to happen.
This is how I would do it. I would build a history of the particles moving on the track. The older the position the smaller the value of the alpha value for the fill. Also for a nicer effect I would reduce the size of the circle.
I hope this is what you need.
PS: I would have loved to have your curve. Since I don't have it I've drawn a different one.
const hypotrochoid = document.getElementById("hypotrochoid");
const ctx = hypotrochoid.getContext("2d");
const cw = (hypotrochoid.width = 300);
const ch = (hypotrochoid.height = 300);
const cx = cw / 2,
cy = ch / 2;
ctx.lineWidth = 1;
ctx.strokeStyle = "#d9d9d9";
// variables for the hypotrochoid
let a = 90;
let b = 15;
let h = 50;
// an array where to save the points used to draw the track
let track = [];
//add points to the track array. This will be used to draw the track for the particles
for (var t = 0; t < 2 * Math.PI; t += 0.01) {
let o = {};
o.x = cx + (a - b) * Math.cos(t) + h * Math.cos((a - b) / b * t);
o.y = cy + (a - b) * Math.sin(t) - h * Math.sin((a - b) / b * t);
track.push(o);
}
// a function to draw the track
function drawTrack(ry) {
ctx.beginPath();
ctx.moveTo(ry[0].x, ry[0].y);
for (let t = 1; t < ry.length; t++) {
ctx.lineTo(ry[t].x, ry[t].y);
}
ctx.closePath();
ctx.stroke();
}
// a class of points that are moving on the track
class Point {
constructor(pos) {
this.pos = pos;
this.r = 3;//the radius of the circle
this.history = [];
this.historyLength = 40;
}
update(newPos) {
let old_pos = {};
old_pos.x = this.pos.x;
old_pos.y = this.pos.y;
//save the old position in the history array
this.history.push(old_pos);
//if the length of the track is longer than the max length allowed remove the extra elements
if (this.history.length > this.historyLength) {
this.history.shift();
}
//gry the new position on the track
this.pos = newPos;
}
draw() {
for (let i = 0; i < this.history.length; i++) {
//calculate the alpha value for every element on the history array
let alp = i * 1 / this.history.length;
// set the fill style
ctx.fillStyle = `rgba(0,0,0,${alp})`;
//draw an arc
ctx.beginPath();
ctx.arc(
this.history[i].x,
this.history[i].y,
this.r * alp,
0,
2 * Math.PI
);
ctx.fill();
}
}
}
// 2 points on the track
let p = new Point(track[0]);
let p1 = new Point(track[~~(track.length / 2)]);
let frames = 0;
let n, n1;
function Draw() {
requestAnimationFrame(Draw);
ctx.clearRect(0, 0, cw, ch);
//indexes for the track position
n = frames % track.length;
n1 = (~~(track.length / 2) + frames) % track.length;
//draw the track
drawTrack(track);
// update and draw the first point
p.update(track[n]);
p.draw();
// update and draw the second point
p1.update(track[n1]);
p1.draw();
//increase the frames counter
frames++;
}
Draw();
canvas{border:1px solid}
<canvas id="hypotrochoid"></canvas>
I'm attempting to detect a canvas click and if the click coordinates match a shape. It works fine, the problem is that when the shapes overlap on the canvas (one shape might be smaller than the other) then both shapes are removed. Is there a way to avoid this and only remove one at a time?
addShape() {
const randomNum = (min, max) => Math.round(Math.random() * (max - min) + min),
randomRad = randomNum(10, 100),
randomX = randomNum(randomRad, this.canvas.width - randomRad);
let shape = new Shape(randomX, randomNum, randomRad);
shape.drawShape();
this.shapes.push(shape);
}
canvasClick() {
if(!this.paused) {
const canvasRect = event.target.getBoundingClientRect(),
clickedX = event.clientX - canvasRect.left,
clickedY = event.clientY - canvasRect.top;
for (let i = 0; i < this.shapes.length; i++) {
if(Math.pow(clickedX - this.shapes[i].x, 2) + Math.pow(clickedY - this.shapes[i].y, 2)
< Math.pow(this.shapes[i].rad,2)) {
this.shapes.splice(i, 1);
}
}
}
}
Thanks in advance for the help!
If I have understood what you what, the solution is pretty simple. Just break the loop after you remove one shape. Something like this:
for (let i = 0; i < this.shapes.length; i++) {
if (
Math.pow(clickedX - this.shapes[i].x, 2)
+ Math.pow(clickedY - this.shapes[i].y, 2)
< Math.pow(this.shapes[i].rad, 2)
) {
this.shapes.splice(i, 1);
break; // <--
}
}
You might also what to store the z position of shapes so that you can first sort them in the terms of z and then run this loop so that you don't remove shape below the clicked shape.
This is how I would do it. Please read the comments in the code. I am using the ctx.isPointInPath method but you may use the formula if you prefer.
The main idea is to exit the loop after finding and deleting the first circle.
I hope it helps.
let ctx = canvas.getContext("2d");
canvas.width = 300;
canvas.height = 300;
ctx.fillStyle = "rgba(0,0,0,.5)";
// the array of the circles
let circles = []
class Circle{
constructor(){
this.x = ~~(Math.random() * canvas.width);
this.y = ~~(Math.random() * canvas.height);
this.r = ~~(Math.random() * (40 - 10 + 1) + 10);
this.draw();
}
draw(){
ctx.beginPath();
ctx.arc(this.x,this.y, this.r, 0,2*Math.PI)
}
}
// create 20 circles
for(i=0;i<20;i++){
let c = new Circle();
ctx.fill()
circles.push(c)
}
canvas.addEventListener("click",(e)=>{
// detect the position of the mouse
let m = oMousePos(canvas, e)
for(let i=0;i<circles.length;i++){
// draw a circle but do not fill
circles[i].draw();
// check if the point is in path
if(ctx.isPointInPath(m.x,m.y)){
//remove the circle from the array
circles.splice(i, 1);
// clear the context
ctx.clearRect(0,0,canvas.width,canvas.height)
// redraw all the circles from the array
for(let j=0;j<circles.length;j++){
circles[j].draw();
ctx.fill()
}
//exit the loop
return;
}
}
})
// a function to detect the mouse position
function oMousePos(canvas, evt) {
var ClientRect = canvas.getBoundingClientRect();
return { //objeto
x: Math.round(evt.clientX - ClientRect.left),
y: Math.round(evt.clientY - ClientRect.top)
}
}
canvas{border:1px solid}
<canvas id="canvas"></canvas>
I'm attempting to draw the rotating line in this canvas animation with trailing opacity but it's not working. I've seen this effect with rectangles and arcs but never with a line, so I'm not sure what I need to add.
function radians(degrees) {
return degrees * (Math.PI / 180);
}
var timer = 0;
function sonar() {
var canvas = document.getElementById('sonar');
if (canvas) {
var ctx = canvas.getContext('2d');
var cx = innerWidth / 2,
cy = innerHeight / 2;
canvas.width = innerWidth;
canvas.height = innerHeight;
//ctx.clearRect(0, 0, innerWidth, innerHeight);
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fillRect(0, 0, innerWidth, innerHeight);
var radii = [cy, cy - 30, innerHeight / 3.33, innerHeight / 6.67];
for (var a = 0; a < 4; a++) {
ctx.beginPath();
ctx.arc(cx, cy, radii[a], radians(0), radians(360), false);
ctx.strokeStyle = 'limegreen';
ctx.stroke();
ctx.closePath();
}
// draw grid lines
for (var i = 0; i < 12; i++) {
var x = cx + cy * Math.cos(radians(i * 30));
var y = cy + cy * Math.sin(radians(i * 30));
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(x, y);
ctx.lineCap = 'round';
ctx.strokeStyle = 'rgba(50, 205, 50, 0.45)';
ctx.stroke();
ctx.closePath();
}
if (timer <= 360) {
timer++;
ctx.beginPath();
ctx.fillstyle = 'limegreen';
ctx.moveTo(cx, cy);
ctx.lineTo(cx + cy * Math.cos(radians(timer)), cy + cy * Math.sin(radians(timer)));
ctx.strokeStyle = 'limegreen';
ctx.stroke();
ctx.closePath();
} else {
timer = 0;
}
requestAnimationFrame(sonar);
}
}
sonar();
jsbin example
Here are two ways to do this: with a gradient and by adding translucent lines.
Sidenote, you should try and only redraw what you need to redraw. I separated the canvases and put one on top of the other so that we don't redraw the grid all the time.
function radians(degrees) {
return degrees * (Math.PI / 180);
}
var timer = 0;
function trail() {
var canvas = document.getElementById('trail');
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, innerWidth, innerHeight);
var cx = innerWidth / 2,
cy = innerHeight / 2;
canvas.width = innerWidth;
canvas.height = innerHeight;
if (timer <= 360) {
timer++;
ctx.beginPath();
ctx.fillstyle = 'limegreen';
ctx.moveTo(cx, cy);
ctx.arc(cx,cy,cy,radians(timer-30),radians(timer));
ctx.lineTo(cx + cy * Math.cos(radians(timer)), cy + cy * Math.sin(radians(timer)));
var gradient = ctx.createLinearGradient(
cx+cy*Math.cos(radians(timer)), cy+cy*Math.sin(radians(timer)),
cx+cy*0.9*Math.cos(radians(timer-30)), cy+cy*0.9*Math.sin(radians(timer-30)));
gradient.addColorStop(0,'limegreen');
gradient.addColorStop(1,'transparent');
ctx.strokeStyle='transparent';
ctx.fillStyle = gradient;
ctx.fill();
ctx.beginPath();
var fade = 10;
for(var i =0;i<fade;i++)
{
ctx.moveTo(cx, cy);
ctx.lineTo(cx+cy*Math.cos(radians(180+timer-i*1.3)),cy+cy*Math.sin(radians(180+timer-i*1.3)));
ctx.strokeStyle ="rgba(50,205,50,0.1)";
ctx.lineWidth=5;
ctx.closePath();
ctx.stroke();
}
} else {
timer = 0;
}
requestAnimationFrame(trail);
}
function sonar() {
var canvas = document.getElementById('sonar');
if (canvas) {
var ctx = canvas.getContext('2d');
var cx = innerWidth / 2,
cy = innerHeight / 2;
canvas.width = innerWidth;
canvas.height = innerHeight;
//ctx.clearRect(0, 0, innerWidth, innerHeight);
var radii = [cy, cy - 30, innerHeight / 3.33, innerHeight / 6.67];
for (var a = 0; a < 4; a++) {
ctx.beginPath();
ctx.arc(cx, cy, radii[a], radians(0), radians(360), false);
ctx.strokeStyle = 'limegreen';
ctx.stroke();
ctx.closePath();
}
// draw grid lines
for (var i = 0; i < 12; i++) {
var x = cx + cy * Math.cos(radians(i * 30));
var y = cy + cy * Math.sin(radians(i * 30));
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(x, y);
ctx.lineCap = 'round';
ctx.strokeStyle = 'rgba(50, 205, 50, 0.45)';
ctx.stroke();
ctx.closePath();
}
}
}
sonar();
trail();
canvas{
position: absolute;
}
<canvas id=sonar></canvas>
<canvas id=trail></canvas>
The problem is that to get this effect, you need to draw a triangle with a gradient along an arc, and you can't do that in a canvas. Gradients must be linear or radial.
The other option is to have an inner loop run each time you want to draw the sweeper, and go backwards from your sweeper line, drawing with slightly less opacity each time. But lets say you want your sweep to cover 15 degrees--obviously, if you have a 100% opacity line at d and a 5% opacity line at d - 15, that doesn't do the trick. So start filling in more lines, and more lines...you will have to draw so many lines to make it seem filled your performance would probably suffer.
My suggestion--you shouldn't have to redraw that on every frame. I would just make a PNG that looks like you want it to, and then place it and just rotate it around the center on each frame. No need to redraw it all the time then. That will be much faster than drawing a bunch of lines.
Canvas stack trails.
Below is a quick demo of how to use a stack of canvases to create a trailing effect.
You have a normal on screen canvas (this FX will not effect it) and then a stack of canvases for the trail FX. Each frame you move to the next canvas in the stack, first slightly clearing it then drawing to it what you want to trail. Then you render that canvas and the one just above it to the canvas.
A point to keep in mind is that the trails can also have a hugh range of FX, like blurring (just render each frame stack on itself slightly offset each time you render to it), zoom in and out trails. Trails on top or trails under. You can change the trail distance and much more.
It is overkill but over kill is fun.
The slider above the demo controls the trail length. Also the code need babel because I dont have time to write it for ES5.
Top slider is trail amount.One under that is trail distance. Trail dist does not transition well. Sorry about that.
//==============================================================================
// helper function
function $(query,q1){
if(q1 !== undefined){
if(typeof query === "string"){
var e = document.createElement(query);
if(typeof q1 !== "string"){
for(var i in q1){
e[i] = q1[i];
}
}else{
e.id = q1;
}
return e;
}
return [...query.querySelectorAll(q1)];
}
return [...document.querySelectorAll(query)];
}
function $$(element,e1){
if(e1 !== undefined){
if(typeof element === "string"){
$(element)[0].appendChild(e1);
return e1;
}
element.appendChild(e1);
return e1;
}
document.body.appendChild(element);
return element;
}
function $E(element,types,listener){
if(typeof types === "string"){
types = types.split(",");
}
element = $(element)[0];
types.forEach(t=>{
element.addEventListener(t,listener)
});
return element;
}
function R(I){
if(I === undefined){
return Math.random();
}
return Math.floor(Math.random()*I);
}
//==============================================================================
//==============================================================================
// answer code
// canvas size
const size = 512;
const trailDist = 10; // There is this many canvases so be careful
var trailDistCurrent = 10; // distance between trails
var clearAll = false;
// create a range slider for trail fade
$$($("input",{type:"range",width : size, min:0, max:100, step:0.1, value:50, id:"trail-amount",title:"Trail amount"}));
$("#trail-amount")[0].style.width = size + "px";
$E("#trail-amount","change,mousemove",function(e){fadeAmount = Math.pow(this.value / 100,2);});
// create a range slider trail distance
$$($("input",{type:"range",width : size, min:2, max:trailDist , step:1, value:trailDist , id:"trail-dist",title:"Trail seperation"}));
$("#trail-dist")[0].style.width = size + "px";
$E("#trail-dist","change,mousemove", function(e){
if(this.value !== trailDistCurrent){
trailDistCurrent= this.value;
clearAll = true;
}
});
$$($("br","")) // put canvas under the slider
// Main canvas
var canvas;
$$(canvas = $("canvas",{width:size,height:size})); // Not jquery. Just creates a canvas
// and adds canvas to the document
var ctx = canvas.getContext("2d");
// Trailing canvas
var trailCanvases=[];
var i =0; // create trail canvas
while(i++ < trailDist){trailCanvases.push($("canvas",{width:size,height:size}));}
var ctxT = trailCanvases.map(c=>c.getContext("2d")); // get context
var topCanvas = 0;
var fadeAmount = 0.5;
// Draw a shape
function drawShape(ctx,shape){
ctx.lineWidth = shape.width;
ctx.lineJoin = "round";
ctx.strokeStyle = shape.color;
ctx.setTransform(shape.scale,0,0,shape.scale,shape.x,shape.y);
ctx.rotate(shape.rot);
ctx.beginPath();
var i = 0;
ctx.moveTo(shape.shape[i++],shape.shape[i++]);
while(i < shape.shape.length){
ctx.lineTo(shape.shape[i++],shape.shape[i++]);
}
ctx.stroke();
}
// Create some random shapes
var shapes = (function(){
function createRandomShape(){
var s = [];
var len = Math.floor(Math.random()*5 +4)*2;
while(len--){
s[s.length] = (R() + R()) * 20 * (R() < 0.5 ? -1 : 1);
}
return s;
}
var ss = [];
var i = 10;
while(i--){
ss[ss.length] = createRandomShape();
}
ss[ss.length] = [0,0,300,0]; // create single line
return ss;
})();
// Create some random poits to move the shapes
var points = (function(){
function point(){
return {
color : "hsl("+R(360)+",100%,50%)",
shape : shapes[R(shapes.length)],
width : R(4)+1,
x : R(size),
y : R(size),
scaleMax : R()*0.2 + 1,
scale : 1,
s : 0,
rot : R()*Math.PI * 2,
dr : R()*0.2 -0.1,
dx : R()*2 - 1,
dy : R()*2 - 1,
ds : R() *0.02 + 0.01,
}
}
var line = shapes.pop();
var ss = [];
var i = 5;
while(i--){
ss[ss.length] = point();
}
var s = ss.pop();
s.color = "#0F0";
s.x = s.y = size /2;
s.dx = s.dy = s.ds = 0;
s.scaleMax = 0.5;
s.dr = 0.02;
s.shape = line;
s.width = 6;
ss.push(s);
return ss;
})();
var frameCount = 0; // used to do increamental fades for long trails
function update(){
// to fix the trail distance problem when fade is low and distance high
if(clearAll){
ctxT.forEach(c=>{
c.setTransform(1,0,0,1,0,0);
c.clearRect(0,0,size,size);
});
clearAll = false;
}
frameCount += 1;
// get the next canvas that the shapes are drawn to.
topCanvas += 1;
topCanvas %= trailDistCurrent;
var ctxTop = ctxT[topCanvas];
// clear the main canvas
ctx.setTransform(1,0,0,1,0,0); // reset transforms
// Fade the trail canvas
ctxTop.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,size,size); // clear main canvas
// slowly blendout trailing layer
if(fadeAmount < 0.1){ // fading much less than this leaves perminant trails
// so at low levels just reduce how often the fade is done
if(((Math.floor(frameCount/trailDistCurrent)+topCanvas) % Math.ceil(1 / (fadeAmount * 10))) === 0 ){
ctxTop.globalAlpha = 0.1;
ctxTop.globalCompositeOperation = "destination-out";
ctxTop.fillRect(0,0,size,size);
}
}else{
ctxTop.globalAlpha = fadeAmount;
ctxTop.globalCompositeOperation = "destination-out";
ctxTop.fillRect(0,0,size,size);
}
ctxTop.globalCompositeOperation = "source-over";
ctxTop.globalAlpha = 1;
// draw shapes
for(var i = 0; i < points.length; i ++){
var p = points[i];
p.x += p.dx; // move the point
p.y += p.dy;
p.rot += p.dr;
p.s += p.ds;
p.dr += Math.sin(p.s) * 0.001;
p.scale = Math.sin(p.s) * p.scaleMax+1;
p.x = ((p.x % size) + size) % size;
p.y = ((p.y % size) + size) % size;
drawShape(ctxTop,p); // draw trailing layer (middle)
}
// draw the trail the most distance from the current position
ctx.drawImage(trailCanvases[(topCanvas + 1)%trailDistCurrent],0,0);
// do it all again.
requestAnimationFrame(update);
}
update();
I want to draw circular balls on an HTML canvas in a pyramid pattern.
Like this:
Fiddle where you can show me the algorithm:
https://jsfiddle.net/ofxmr17c/3/
var canvas = document.getElementById('canvas');
canvas.width = 400;
canvas.height = 400;
var ctx = canvas.getContext('2d');
var balls = [];
var ballsLength = 15;
var Ball = function() {
this.x = 0;
this.y = 0;
this.radius = 10;
};
Ball.prototype.draw = function(x, y) {
this.x = x;
this.y = y;
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
};
init();
function init() {
for (var i = 0; i < ballsLength; i++) {
balls.push(new Ball());
}
render();
}
function render() {
for (var i = 1; i <= ballsLength; i++) {
if (i >= 1 && i <= 5) {
balls[i].draw(i * 20 + balls[i].radius, 20 + balls[i].radius);
}
if (i >= 6 && i <= 9) {
balls[i].draw(i * 20 + balls[i].radius, 20 + balls[i].radius * 2);
}
if (i >= 10 && i <= 12) {
balls[i].draw(i * 20 + balls[i].radius, 20 + balls[i].radius * 3);
}
if (i >= 13 && i <= 14) {
balls[i].draw(i * 20 + balls[i].radius, 20 + balls[i].radius * 4);
}
if (i == 15) {
balls[i].draw(i * 20 + balls[i].radius, 20 + balls[i].radius * 5);
}
}
window.requestAnimationFrame(render);
}
canvas {
border: 1px solid #333;
}
<canvas id="canvas"></canvas>
I have Ball class with x, y and radius variables:
var Ball = function() {
this.x = 0;
this.y = 0;
this.radius = 10;
};
Then I have method of the Ball class which draws the balls on the canvas:
Ball.prototype.draw = function(x, y) {
this.x = x;
this.y = y;
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
};
I want to create method which will place any number of balls into a pyramid.
The live demo below shows how to pack an arbitrary number of balls into a pyramid using a bit of trigonometry. To change the amount of layers in the pyramid (and thus the number of balls), edit the NUM_ROWS variable.
This is how it looks when it's done:
Live Demo:
var canvas = document.getElementById('canvas');
canvas.width = 400;
canvas.height = 400;
var ctx = canvas.getContext('2d');
var balls = [];
var ballsLength = 15;
var Ball = function() {
this.x = 0;
this.y = 0;
this.radius = 10;
};
Ball.prototype.draw = function(x, y) {
this.x = x;
this.y = y;
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
};
init();
function init() {
for (var i = 0; i < ballsLength; i++) {
balls.push(new Ball());
}
render();
}
function render() {
var NUM_ROWS = 5;
for (var i = 1; i <= NUM_ROWS; i++) {
for (var j = 0; j < i; j++) {
balls[i].draw(j * balls[0].radius * 2 + 150 - i * balls[0].radius, -(i * balls[0].radius * 2 * Math.sin(Math.PI / 3)) + 150);
}
}
//window.requestAnimationFrame(render);
}
canvas {
border: 1px solid #333;
}
<canvas id="canvas"></canvas>
JSFiddle Version: https://jsfiddle.net/ofxmr17c/6/
A billiard pyramid like this is always made with some known facts:
Each row always contains one more ball than the previous
It's an equilateral equal angled (sp? in english?) triangle which means next row always starts offset 60°
So we can make a vector (everything else in a billiard game would very much involve vectors so why not! :) ) for the direction of the next row's start point like so:
var deg60 = -60 / 180 * Math.PI; // -60°, up-right direction
var v = {
x: radius * Math.cos(deg60),
y: radius * Math.sin(deg60)
}
Then the algorithm would be (driven by total number of balls):
Start with a max limit of 1 for first row
Plot balls until max limit for row is reached
Then, add one to the max limit
Reset row count
Move position to beginning of last row + vector
Repeat until number of balls is reached
Result:
Example
var ctx = c.getContext("2d"),
radius = 9, // ball radius
deg = -60 / 180 * Math.PI, // direction of row start -60°
balls = 15, // number of balls to draw
drawn = 0, // count balls drawn on current row
rowLen = 1, // max length of current row (first=1)
x = 150, // start point
y = 140,
cx = 150, cy =140, // replicates start point + offsets
v = { // vector
x: radius * 2 * Math.cos(deg),
y: radius * 2 * Math.sin(deg)
},
i;
for(i = 0; i < balls; i++) {
drawBall(cx, cy); // draw ball
cx -= radius * 2; // move diameter of ball to left (in this case)
drawn++; // increase balls on row count
if (drawn === rowLen) { // reached max balls for row?
cx = x + v.x * rowLen; // increase one row
cy = y + v.y * rowLen;
drawn = 0; // reset ball count for row
rowLen++; // increase row limit
}
}
ctx.fillStyle = "#D70000";
ctx.fill();
function drawBall(x, y) {
ctx.moveTo(x + radius, y); ctx.arc(x, y, radius, 0, 6.28);
ctx.closePath();
}
<canvas id=c height=300></canvas>
If you want more flexibility in terms of rotation you can simply swap this line:
cx -= radius * 2;
with a vector perpendicular (calculation not shown) to the first vector so:
cx += pv.x;
cy += pv.y;