I'm currently trying to make a Ying and Yang symbol spin using a circular path. SO far I have made the medium and smaller ones rotate just fine. However, the stationary arc's are wrecking the illusion. Here is an open link to see my current code.
https://editor.p5js.org/Nathan65bmx/sketches/PAu3xx6Bd
Just looking for someone to help me make it look like it is rotating properly.
Draw all shapes from a common central point, then use the rotate() function. https://p5js.org/reference/#/p5/rotate Here's the link.
Do ask if you need help modifying the code.
[EDIT]
Here's the working version
function setup() {
createCanvas(600, 600);
angleMode(DEGREES);
a = 0;
x = 180;
}
let ANGLE = 0
let a;
let x;
function draw() {
background(180, 13, 123);
//Big Circle
noStroke();
//Change starts from here
push();
translate(300, 300);
rotate(a);
fill("black");
arc(0, 0, 300, 300, 0, x);
fill("white")
arc(0, 0, 300, 300, x,0);
pop();
a+=2;
//Till here
// Medium Circles
fill("black");
let CENTRE_X4 = width / 2;
let CENTRE_Y4 = height / 2;
let RADIUS4 = 75;
let X4 = RADIUS4 * cos(ANGLE);
let Y4 = RADIUS4 * sin(ANGLE);
ellipse(CENTRE_X4 + X4, CENTRE_Y4 + Y4, 150);
fill("white");
let CENTRE_X3 = width / 2;
let CENTRE_Y3 = height / 2;
let RADIUS3 = 75;
let X3 = RADIUS3 * cos(ANGLE);
let Y3 = RADIUS3 * sin(ANGLE);
ellipse(CENTRE_X3 - X3, CENTRE_Y3 - Y3, 150);
// Small Circles
fill("white");
let CENTRE_X = width / 2;
let CENTRE_Y = height / 2;
let RADIUS = 75;
let X = RADIUS * cos(ANGLE);
let Y = RADIUS * sin(ANGLE);
ellipse(CENTRE_X + X, CENTRE_Y + Y, 50);
fill("black");
let CENTRE_X2 = width / 2;
let CENTRE_Y2 = height / 2;
let RADIUS2 = 75;
let X2 = RADIUS2 * cos(ANGLE);
let Y2 = RADIUS2 * sin(ANGLE);
ellipse(CENTRE_X2 - X2, CENTRE_Y2 - Y2, 50);
ANGLE = ANGLE + 2;
}
All the edits have been done using the push() & pop() and rotate() functions.
Hope this has helped!
My answer is not adding anything new to Ruskin's great answer suggesting rotate() as well as push()/pop(), but wanted to mention that you could isolate the drawing instructions into a re-usable function and additionally simply reduce some of complexity and repetition (see D.R.Y):
function setup() {
createCanvas(600, 600);
angleMode(DEGREES);
}
function draw() {
background (200, 13, 123);
// isolate coordinate system
push();
// move everything to the center
translate(width / 2, height / 2);
// rotate everything from the center
rotate(frameCount % 360);
// draw ying Yang
drawYingYang(300);
// return to the original coordinate system (0,0 = top left)
pop();
}
function drawYingYang(outerDiameter){
let innerYOffset = outerDiameter / 4;
let outerRadius = outerDiameter / 2;
let innerDiameter = innerYOffset / 1.5;
// Big Circle
noStroke();
fill("black");
arc(0, 0, outerDiameter, outerDiameter, -90, -270);
fill("white")
arc(0, 0, outerDiameter, outerDiameter, 90, 270);
// Medium Circles
fill("black");
ellipse(0, innerYOffset, outerRadius);
fill("white");
ellipse(0, - innerYOffset, outerRadius);
// Small Circles
fill("white");
ellipse(0, innerYOffset, innerDiameter);
fill("black");
ellipse(0, - innerYOffset, innerDiameter);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>
If that's the only thing you want to draw, removing push()/pop() won't make a difference visually, however, if you want to draw other shapes it will much easier to have independent control over where each shape is drawn
Related
I’m working on a project with p5.js where I draw a circle, draw arcs (straight red lines) to separate the circle then another arc between each of the red lines (blue lines). The idea looks like the included image below:
What I'm confused about is how to position the labels in the circle drawing so that they're positioned in each segment inside the circle but outside the blue arcs. My question is how do I add text labels to this figure so that it looks like the image below?
Here is the shortened code to produce the first image (circle without the labels) so far:
function setup() {
createCanvas(400, 400);
}
function draw() {
background(255);
let startX = 50;
let startY = 50;
let data = [1, 2, 3, 4];
let width = 80;
let angle = -Math.PI / 2;
let radianPer = Math.PI * 2 / Object.keys(data).length;
noStroke();
fill(255);
ellipse(startX, startY, width, width);
Object.keys(data).forEach(i => {
fill(255);
stroke(255, 0, 0);
arc(startX, startY, width, width, angle, angle + radianPer, PIE);
fill(255);
stroke(0, 0, 255);
arc(startX, startY, width / 2, width / 2, angle, angle + radianPer, PIE);
angle += radianPer;
// add label here
});
}
Edit (02/05/22): updated code to match the screenshot image example.
Displaying a label in the middle of a segment of an arc involves using the angle for the middle of that arc with the sine and cosine functions to find the X and Y coordinates. For more information see the trigonometric functions article on wikipedia.
function setup() {
createCanvas(400, 400);
// Text settings
textAlign(CENTER, CENTER);
}
function draw() {
background(255);
let startX = 50;
let startY = 50;
let data = [1, 2, 3, 4];
let width = 80;
let angle = -Math.PI / 2;
let radianPer = (Math.PI * 2) / Object.keys(data).length;
noStroke();
fill(255);
ellipse(startX, startY, width, width);
Object.keys(data).forEach((i) => {
fill(255);
stroke(255, 0, 0);
arc(startX, startY, width, width, angle, angle + radianPer, PIE);
fill(255);
stroke(0, 0, 255);
arc(startX, startY, width / 2, width / 2, angle, angle + radianPer, PIE);
// add label here
let textAngle = angle + radianPer / 2;
// Use sine and cosine to determine the position for the text
// Since sine is opposite / hypotenuse, taking the sine of the angle and
// multiplying by distance gives us the vertical offset (i.e. the Y
// coordinate).
// Likewise with cosine for the X coordinate
noStroke();
fill(0);
text(
data[i].toString(),
startX + cos(textAngle) * width / 2 * 0.75,
startY + sin(textAngle) * width / 2 * 0.75
);
// Don't update angle until after calculating the angle for the label
angle += radianPer;
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
I am trying to create an effect where the total height of a group of shapes is constant (say 300), whilst each shape within that group has a dynamic, oscillating, height. In one instance, maybe the middle shape is 'taller' whilst the outer shapes are shorter.
This desired effect is similar to if you held a slinky, with each end in one hand fixed at 30cm apart, and then shook it around: the total height remains the same (30cm) but the 'sections' inside the slinky are having their individual heights bounce up and down.
My attempts so far use the sin function to get an oscillating number as an angle value increases. This works for the sections, but I can't figure out how to maintain the constant overall height. See the code snippet below; red (and the tip of the bottom black triangle) should always be touching the bottom of the container.
// Prepare variables for angles, separated by 1
let a1 = 0;
let a2 = 1;
let a3 = 2;
let a4 = 3;
let a5 = 4;
// Prepare shape width
let shapeW = 150;
function setup() {
createCanvas(300, 300);
rect(10, 10, 10, 10);
}
function draw() {
background(240);
noStroke();
// Use the sin ratio to 'oscillate' a height value between 0 and 60
let x1 = map(sin(a1), -1, 1, 0, height / 5);
let x2 = map(sin(a2), -1, 1, 0, height / 5);
let x3 = map(sin(a3), -1, 1, 0, height / 5);
let x4 = map(sin(a4), -1, 1, 0, height / 5);
let x5 = map(sin(a5), -1, 1, 0, height / 5);
// Store these in an array so I can loop through
let listOfValues = [x1, x2, x3, x4, x5];
// Loop through and draw shapes
push();
translate((width / 2) - shapeW / 2, 0)
for (let i = 0; i < listOfValues.length; i++) {
fill(255, 0, 0);
rect(0, 0, shapeW, listOfValues[i]);
fill(0)
triangle(0, 0, shapeW / 2, listOfValues[i], shapeW, 0)
translate(0, listOfValues[i]);
}
pop();
// Increment each angle by the same amount
let incAmount = 0.1;
a1 += incAmount;
a2 += incAmount;
a3 += incAmount;
a4 += incAmount;
a5 += incAmount;
}
html,
body {
margin: 0;
padding: 0;
}
canvas {
display: block;
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.0/lib/p5.js"></script>
With help, I've found the solution is to use binomial coefficients. That is achieved here via a binomial() function. The only caveat is that the number of 'sections' (represented as n) must be an even number.
let angle = 0;
let N;
let containerW = 300;
let shapeW = 150;
let n = 6;
let speed = 0.0075;
function setup() {
createCanvas(containerW, containerW);
N = n * binomial(n, n / 2);
}
function draw() {
background(240);
noStroke();
let listOfVals = [];
for (let i = 0; i < n; i++) {
listOfVals.push(x(i + 1));
}
push();
translate(width / 2 - shapeW / 2, 0);
for (let i = 0; i < listOfVals.length; i++) {
fill(255, 0, 0);
rect(0, 0, shapeW, listOfVals[i] * height);
fill(0);
triangle(0, 0, shapeW / 2, listOfVals[i] * height, shapeW, 0);
translate(0, listOfVals[i] * height);
}
pop();
// Increment angle
angle += speed;
}
function x(k) {
return (2 ** n * sin(angle + (k * PI) / n) ** n) / N;
}
function binomial(n, k) {
if (typeof n !== "number" || typeof k !== "number") return false;
var coeff = 1;
for (var x = n - k + 1; x <= n; x++) coeff *= x;
for (x = 1; x <= k; x++) coeff /= x;
return coeff;
}
html,
body {
margin: 0;
padding: 0;
}
canvas {
display: block;
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.0/lib/p5.js"></script>
Nice self answer (+1).
This is more of an idea for a slightly different approach, hopefully with a few simplifications:
// Prepare shape width
let shapeW = 150;
// Prepare shape height
let shapeH;
// total number of shapes
let numShapes = 5;
// Increment each angle offset by the same amount
let incAmount = 0.05;
function setup() {
createCanvas(300, 300);
rect(10, 10, 10, 10);
// assign shape height after sketch height has been set
shapeH = height / 5;
}
function draw() {
background(240);
noStroke();
// Loop through and draw shapes
push();
// horizontally center shapes
translate((width - shapeW) / 2, 0);
// for each shape
for (let i = 0; i < numShapes; i++) {
// map the current height to the increment asdasdakrk
let currentH = map(sin(i + (frameCount * incAmount)), -1, 1, 0, shapeH);
fill(255, 0, 0);
rect(0, 0, shapeW, currentH);
fill(0)
triangle(0, 0, shapeW / 2, currentH, shapeW, 0)
translate(0, currentH);
}
pop();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
The above is using the same logic, mostly removing the need for the a1, a2, a3, a4, a5 values as they coincide with the i counter for each shape.
A visual way I think about it having to connect the tip of one triangle with the base of the next triangle (or the current triangle's base being the same as the the previous triangle tip's y position):
// Prepare shape width
let shapeW = 150;
// Prepare shape height
let shapeH;
// total number of shapes
let numShapes = 5;
// Increment each angle offset by the same amount
let incAmount = 0.05;
// sine driven scales
let minYScale = 0.5;
let maxYScale = 2.0;
function setup() {
createCanvas(300, 300);
rect(10, 10, 10, 10);
// assign shape height after sketch height has been set
shapeH = height / 5;
}
function draw() {
background(240);
noStroke();
// Loop through and draw shapes
push();
// horizontally center shapes
translate((width - shapeW) / 2, 0);
// draw red background
fill(255, 0, 0);
rect(0, 0, shapeW, height);
// remember where the previous array base was
let lastY = 0;
// for each shape
for (let i = 0; i < numShapes; i++) {
// map the current y scale to the increment
let currentYScale = map(sin(i + (frameCount * incAmount)), -1, 1, minYScale, maxYScale);
// compute the current scale based on the sine scalar
let currentH = currentYScale * shapeH;
fill(0);
triangle(0, lastY,
shapeW / 2, lastY + currentH,
shapeW, lastY);
// update absolute y position of the arrow base
lastY += currentH;
// optional: for debugging only, visualise lastY
if(mouseIsPressed) rect(-shapeW, lastY, width + shapeW, 3);
}
pop();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
I'm trying to animate a spiral using a line, but can only seem to get it to work using ellipses.
Does anyone know how to replace the ellipse() with line()?
here is the code:
var angle = 0.0;
var offset = 60;
var scalar = 10;
var speed = 0.05;
function setup() {
createCanvas(600, 120);
fill(0);
}
function draw() {
var x = offset + cos(angle) * scalar;
var y = offset + sin(angle) * scalar;
ellipse( x, y, 2, 2);
angle += speed;
scalar += speed;
}
Assuming you would like to draw the entire spiral instantaneously using line segments, the you simply need a for loop that calculates the x and y coordinates for the current and next point in the spiral for some increment of change, and then draw lines between each pair of points. There are certainly numerous ways to write such a for loop, depending on what the constrains are (do you want a specific number of rings in your spiral? a specific number of degrees of rotation?), but importantly the bigger your increment of change the less smooth your spiral will look. Here is an example that uses the mouse position to determine the number of rings and the size of the change increments:
function setup() {
createCanvas(windowWidth, windowHeight);
stroke(0);
strokeWeight(4);
textAlign(LEFT, TOP);
}
function draw() {
background(255);
// let the horizontal mouse position indicate the
// size of the steps
let speed = map(mouseX, 0, width, 0.01, 1, true);
// let the vertical mouse position indicate the
// total amount of rotation
let maxRotation = map(mouseY, 0, height, TWO_PI, TWO_PI * 50, true);
push();
noStroke();
fill('red');
text(`Rings: ${(maxRotation / TWO_PI).toFixed(1)}, Speed: ${speed.toFixed(2)}`, 10, 10);
pop();
translate(width / 2, height / 2);
let scalar = 10;
if (speed <= 0) {
console.error('cannot have <= 0 speed');
return;
}
for (let angle = 0; angle < maxRotation; angle += speed, scalar += speed) {
const x = cos(angle) * scalar;
const y = sin(angle) * scalar;
const x2 = cos(angle + speed) * (scalar + speed);
const y2 = sin(angle + speed) * (scalar + speed);
line(x, y, x2, y2);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
I'm developing a small minigolf game, where the user can shoot moving the cursor around to set an angle, and the force applied will be the length of an arrow (less force when the cursor is closer to the ball). You can check exactly how it works here: https://imgur.com/a/AQ1pi
I have figured out how to rotate the arrow sprite to follow the cursor but I don't know yet how to make it move around the ball, right now it's just rotating in its anchor, in this case the head of the arrow.
I'm using Panda.js (a Pixi.js based framework) to develop the game, but its API is similar to the native Canvas functions. I don't need an exact implementation (that's why I'm not posting any code), but I would like to get some ideas about how to rotate the sprite around a point in a given radius. In this case, the point would be the center of the ball, and the radius will be the ball radius. Thanks!
You set the point of rotation with ctx.translate or ctx.setTransform then apply the rotation with ctx.rotate(ang); Then draw the image offset so that the point of rotation is at (0,0). Ie if you want the point of rotation to be at image coordinates (100,50) then render at ctx.drawImage(image,-100,-50);
To get the angle from a point to the mouse use Math.atan2
requestAnimationFrame(update);
// draws rotated image at x,y.
// cx, cy is the image coords you want it to rotate around
function drawSprite(image, x, y, cx, cy, rotate) {
ctx.setTransform(1, 0, 0, 1, x, y);
ctx.rotate(rotate);
ctx.drawImage(image, -cx, -cy);
ctx.setTransform(1, 0, 0, 1, 0, 0); // restore defaults
}
// function gets the direction from point to mouse and draws an image
// rotated to point at the mouse
function rotateAroundPoint(x, y, mouse) {
const dx = mouse.x - x;
const dy = mouse.y - y;
const dir = Math.atan2(dy, dx);
drawSprite(arrow, x, y, 144, 64, dir);
}
// Main animation loop.
function update(timer) {
globalTime = timer;
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
strokeCircle(150, 75, 10);
rotateAroundPoint(150, 75, mouse);
requestAnimationFrame(update);
}
//=====================================================
// All the rest is unrelated to the answer.
const ctx = canvas.getContext("2d");
const mouse = { x: 0, y: 0, button: false };
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
function mouseEvents(e) {
mouse.bounds = canvas.getBoundingClientRect();
mouse.x = e.pageX - mouse.bounds.left - scrollX;
mouse.y = e.pageY - mouse.bounds.top - scrollY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"), c.width = w, c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w, h), c.ctx = c.getContext("2d"), c);
const drawPath = (ctx, p) => {var i = 0;while (i < p.length) {ctx.lineTo(p[i++], p[i++])}};
const strokeCircle = (l,y=ctx,r=ctx,c=ctx) =>{if(l.p1){c=y; r=leng(l);y=l.p1.y;l=l.p1.x }else if(l.x){c=r;r=y;y=l.y;l=l.x}c.beginPath(); c.arc(l,y,r,0,Math.PI*2); c.stroke()};
const aW = 10;
const aH = 20;
const ind = 5;
const arrow = CImageCtx();
arrow.ctx.beginPath();
drawPath(arrow.ctx, [
ind, 64 - aW,
128 - ind - aH, 64 - aW,
128 - ind - aH, 64 - aH,
128 - ind, 64,
128 - ind - aH, 64 + aH,
128 - ind - aH, 64 + aW,
ind, 64 + aW,
]);
arrow.ctx.fillStyle = "red";
arrow.ctx.fill();
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var globalTime;
canvas {
border: 2px solid black;
}
<canvas id="canvas"></canvas>
I need to make the following shape in HTML5 canvas. I have tried using cubic bezier arcs and also clipping two circles.
How can I make this shape?
Here's my work in progress, just cant get it right
https://codepen.io/matt3224/pen/oeXbdg?editors=1010
var canvas = document.getElementById("canvas1");
var ctx1 = canvas.getContext("2d");
ctx1.lineWidth = 2;
ctx1.beginPath();
ctx1.bezierCurveTo(4, 42, 0, 0, 42, 4);
ctx1.moveTo(4, 42);
ctx1.bezierCurveTo(4, 42, 0, 84, 42, 84);
ctx1.stroke();
var canvas = document.getElementById("canvas2");
var ctx2 = canvas.getContext("2d");
ctx2.lineWidth = 2;
ctx2.beginPath();
ctx2.arc(55, 75, 50, 0, Math.PI * 2, true);
ctx2.moveTo(165, 75);
ctx2.arc(75, 75, 50, 0, Math.PI * 2, true);
ctx2.fill();
Circle circle boolean operation.
Incase anyone is interested in a programmatic solution the example below finds the intercept points of the two circles and uses those points to calculate the start and end angles for the outer and inner circle.
This is a little more flexible than a masking solution as it give you a path.
Snippet shows circle, move mouse over circle to see crescent solution. Not the stroke that would not be available if using a masking solution.
const PI2 = Math.PI * 2;
const ctx = canvas.getContext("2d");
canvas.height = canvas.width = 400;
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
const m = mouse;
const bounds = canvas.getBoundingClientRect();
m.x = e.pageX - bounds.left - scrollX;
m.y = e.pageY - bounds.top - scrollY;
m.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : m.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
// generic circle circle intercept function. Returns undefined if
// no intercept.
// Circle 1 is center x1,y1 and radius r1
// Circle 2 is center x2,y2 and radius r2
// If points found returns {x1,y1,x2,y2} as two points.
function circleCircleIntercept(x1,y1,r1,x2,y2,r2){
var x = x2 - x1;
var y = y2 - y1;
var dist = Math.sqrt(x * x + y * y);
if(dist > r1 + r2 || dist < Math.abs(r1-r2)){
return; // no intercept return undefined
}
var a = (dist * dist - r1 * r1 + r2 *r2) / ( 2 * dist);
var b = Math.sqrt(r2 * r2 - a * a);
a /= dist;
x *= a;
y *= a;
var mx = x2 - x;
var my = y2 - y;
dist = b / Math.sqrt(x * x + y * y);
x *= dist;
y *= dist;
return {
x1 : mx-y,
y1 : my+x,
x2 : mx+y,
y2 : my-x,
};
}
// draws a crescent from two circles if possible
// If not then just draws the first circle
function drawCrescent(x1,y1,r1,x2,y2,r2){
// The circle circle intercept finds points
// but finding the angle of the points does not consider
// the rotation direction and you end up having to do a lot of
// checking (if statments) to determin the correct way to draw each circle
// the following normalises the direction the circle are from each other
// thus making the logic a lot easier
var dist = Math.hypot(x2-x1,y2-y1);
var ang = Math.atan2(y2-y1,x2-x1);
var intercepts = circleCircleIntercept(x1,y1,r1,x1 + dist,y1,r2);
if(intercepts === undefined){
ctx.beginPath();
ctx.arc(x1, y1, r1, 0, PI2);
if(dist < r1){
ctx.moveTo(x2 + r2, y2);
ctx.arc(x2, y2, r2, 0, PI2, true);
}
ctx.fill();
ctx.stroke();
return;
}
// get the start end angles for outer then inner circles
const p = intercepts;
var startA1 = Math.atan2(p.y1 - y1, p.x1 - x1) + ang;
var endA1 = Math.atan2(p.y2 - y1, p.x2 - x1) + ang;
var startA2 = Math.atan2(p.y1 - y1, p.x1 - (x1 + dist)) + ang;
var endA2 = Math.atan2(p.y2 - y1, p.x2 - (x1 + dist)) + ang;
ctx.beginPath();
if(endA1 < startA1){
ctx.arc(x1, y1, r1, startA1, endA1);
ctx.arc(x2, y2, r2, endA2, startA2, true);
}else{
ctx.arc(x2, y2, r2, endA2, startA2);
ctx.arc(x1, y1, r1, startA1, endA1,true);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
}
const outerRadius = 100;
const innerRadius = 80;
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var globalTime;
ctx.font = "32px arial";
ctx.textAlign = "center";
ctx.lineJoin = "round";
ctx.lineWidth = 8;
ctx.strokeStyle = "#999";
// main update function
function mainLoop(timer){
globalTime = timer;
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.fillStyle = "black";
ctx.fillRect(0,0,w,h);
ctx.fillStyle = "white";
ctx.fillText("Move mouse over circle",cw,40);
drawCrescent(cw, ch-40, outerRadius, mouse.x, mouse.y, innerRadius);
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas>
Solved it using globalCompositeOperation
https://codepen.io/matt3224/pen/oeXbdg?editors=1010