This is the result i want to make with divs
How can i achieve this result?
Edit: my goal was not to use divs only that i didn't want to use canvas. But i haven't thought about SVG's so thank you!
Here's a quick and dirty alternative example using SVG arcs
const cx = 100; // Circle centre
const cy = 100;
const width = 40; // Width of line
const radius = 100; // Radius of circle
const TwoPi = Math.PI * 2;
// Compute circumference
const circ = TwoPi * radius;
const height = circ / 12; // Length of each segment
const parent = document.getElementById("curve");
for (let i = 0; i < circ; i += height) {
let seg = document.createElementNS("http://www.w3.org/2000/svg", "path");
let rs = (i / circ) * TwoPi;
let re = ((i + height) / circ) * TwoPi;
let ss = Math.sin(rs);
let cs = Math.cos(rs);
let se = Math.sin(re);
let ce = Math.cos(re);
// Build wedge path element
seg.setAttribute("d",
`M${(cs * radius) + cx},${(ss * radius) + cy}` +
`A${radius},${radius} ${((re - rs) / Math.PI) * 180},0,1 ${(ce * radius) + cx},${(se * radius) + cy}` +
`L${(ce * (radius - width)) + cx},${(se * (radius - width)) + cy}` +
`A${radius - width},${radius - width} ${((re - rs) / Math.PI) * -180},0,0 ${(cs * (radius - width)) + cx},${(ss * (radius - width)) + cy}z`
);
seg.setAttribute("class", "pathSeg");
parent.appendChild(seg);
}
.pathSeg { stroke: black; stroke-width: 3px; fill: white }
.pathSeg:hover { fill: red }
<svg width="200" height="200" viewBox="0 0 200 200">
<g id="curve"></g>
</svg>
HTML elements aren't really suited to this kind of layout since they're inherently rectangular whereas your segments have curved boundaries.
CSS transforms will only allow you to apply affine transformations which affect the whole shape equally and cannot create these kinds of curves.
SVG or Canvas would be much better fits for this kind of drawing depending on what you're planning on doing with it.
If you really need to go the HTML element route, then your best bet would be to layout the divs and apply clipping masks to them to accomplish the curved sections. Here's a basic example of plotting divs along a circle path:
const cx = 100; // Circle centre
const cy = 100;
const width = 40; // Width of line
const height = 30; // Length of each segment
const radius = 100; // Radius of circle
const TwoPi = Math.PI * 2;
// Compute circumference
const circ = TwoPi * radius;
const parent = document.documentElement;
for (let i = 0; i < circ; i += height) {
let div = document.createElement("div");
div.className = "pathSeg";
div.style.width = `${width}px`;
div.style.height = `${height}px`;
div.style.transform = `translate(${cx}px, ${cy}px) rotate(${(i / circ) * 360}deg) translate(${radius}px, 0)`;
parent.appendChild(div);
}
.pathSeg {
position: absolute;
border: 1px solid black;
}
Related
I have a code to draw spiral with points
var c = document.getElementById("myCanvas");
var cxt = c.getContext("2d");
var centerX = 400;
var centerY = 400;
cxt.moveTo(centerX, centerY);
var count = 0;
var increment = 3/32;
var distance = 10;
for (theta = 0; theta < 50; theta++) {
var newX = centerX + distance * Math.cos((theta) * 4 * Math.PI * increment );
var newY = centerY + distance * Math.sin(((theta)) * 4 * Math.PI * increment );
cxt.fillText("o", newX, newY);
count++;
if (count % 4 === 0) {
distance = distance + 10;
}
}
cxt.stroke();
<canvas id="myCanvas" width="800" height="800" style="border:1px solid #c3c3c3;"></canvas>
enter image description here
Notice how many time i change the increment value, there're always a line that have more or less points than others
increment = 5/32;
enter image description here
Is this possible to draw a perfect spiral with all lines has the same lenght with each other?
There are a quite a few issues here. Like #Anytech said, you need to first decide how many arms (strings of dots) you want. In your screenshot, it looks like you have 5 arms, but you probably got that by accident. I've replaced the "o" with the distance to help visualize the problem:
var c = document.getElementById("myCanvas");
var cxt = c.getContext("2d");
var centerX = 200;
var centerY = 200;
cxt.moveTo(centerX, centerY);
var count = 0;
var increment = 3/32;
var distance = 10;
for (theta = 0; theta < 50; theta++) {
var newX = centerX + distance * Math.cos((theta) * 4 * Math.PI * increment );
var newY = centerY + distance * Math.sin(((theta)) * 4 * Math.PI * increment );
cxt.fillText(distance, newX, newY);
count++;
if (count % 4 === 0) {
distance = distance + 10;
}
}
cxt.stroke();
<canvas id="myCanvas" width="400" height="400" style="border:1px solid #c3c3c3;"></canvas>
As you can see, the first four dots are at distance 10, the 5th one is at distance 20, from there, you've already broken the rhythm.
Assuming you still want 5 arms, increase the distance every 5 dots by checking count % 5 === 0, instead of count % 4 === 0.
var c = document.getElementById("myCanvas");
var cxt = c.getContext("2d");
var centerX = 200;
var centerY = 200;
cxt.moveTo(centerX, centerY);
var count = 0;
var increment = 3/32;
var distance = 10;
for (theta = 0; theta < 50; theta++) {
var newX = centerX + distance * Math.cos((theta) * 4 * Math.PI * increment );
var newY = centerY + distance * Math.sin(((theta)) * 4 * Math.PI * increment );
cxt.fillText(distance, newX, newY);
count++;
if (count % 5 === 0) {
distance = distance + 10;
}
}
cxt.stroke();
<canvas id="myCanvas" width="400" height="400" style="border:1px solid #c3c3c3;"></canvas>
Also, a full circle is 2 * Math.PI. If you can use Math.cos((theta) * 2 * Math.PI * increment), increment becomes the arc the dot will travel after each paint. If increment is 1/5, you will get five straight lines. If increment is a little bit more than 1/5, you will get the curve effect you want.
And one final detail, we usually call context ctx, instead of cxt. Combining all of the above, the output looks like this
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var centerX = 200;
var centerY = 200;
ctx.moveTo(centerX, centerY);
var count = 0;
var increment = 1.02/5;
var distance = 10;
for (theta = 0; theta < 50; theta++) {
var newX = centerX + distance * Math.cos((theta) * 2 * Math.PI * increment );
var newY = centerY + distance * Math.sin(((theta)) * 2 * Math.PI * increment );
ctx.fillText('o', newX, newY);
count++;
if (count % 5 === 0) {
distance = distance + 10;
}
}
ctx.stroke();
<canvas id="myCanvas" width="400" height="400" style="border:1px solid #c3c3c3;"></canvas>
EDIT
After having a chat with the question raiser, I realize the problem also has to to with fillText using the bottom-left corner of the string as the starting point for the paint. To address the issue, we must plot actual circles, instead of letter 'o'.
Here is the final result (concentric circles added to show perfect symmetry)
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var centerX = 200;
var centerY = 200;
ctx.moveTo(centerX, centerY);
var count = 0;
var increment = 1.05/5;
var distance = 10;
for (theta = 0; theta < 50; theta++) {
var newX = centerX + distance * Math.cos((theta) * 2 * Math.PI * increment );
var newY = centerY + distance * Math.sin(((theta)) * 2 * Math.PI * increment );
ctx.textAlign = "center";
//ctx.fillText('o', newX, newY); <== this will be off-center
ctx.beginPath();
ctx.strokeStyle = "#000";
ctx.arc(newX, newY, 2, 0, Math.PI * 2, true)
ctx.stroke();
count++;
if (count % 5 === 0) {
ctx.strokeStyle = "#cccccc";
ctx.beginPath();
ctx.arc(200, 200, distance, 0, Math.PI * 2, true)
ctx.stroke();
distance = distance + 10;
}
}
ctx.stroke();
<canvas id="myCanvas" width="400" height="400" style="border:1px solid #c3c3c3;"></canvas>
The circumference of a circle is 2 * PI * radius.
We can use this to determine what angle step to make to cover a distance on the circumference. Thus the angle that sweeps a given distance along the curve is distance / radius (Note this is not the straight line distance, but the distance ALONG the CURVE)
Though you are creating a spiral and the distance for an angle step is a little longer (as the line moves outward) the approximation will suffice for the human eye.
So changing your code with the following steps
Remove increment
Change distance to radius
Rather than limit the number of turns, we will use the radius to limit the max radius of the spiral. Adding maxRadius = 350 to fit the canvas
Add lineLength to set the approx distance between each point in pixels.
Change the for loop to a while loop as we are going to step angle inside the loop.
Rename theta to angle (we are programmers not mathematicians)
Rather than use a character to draw each point we will create a path of arcs so that the positioning can be precise. That will add some 2D context setup code.
Rather than step out every 4 points (as that no longer will work) we will make the radius a function of angle. With that add some constants to control the radius function.
radiusMin defines the min radius (starting radius)
radiusScale defines the rate per turn in pixel that the spiral moves out.
Remove count as its no longer needed
Inside the loop calculate the radius. As we have difined the radius growth as a rate per turn we divide the radiusScale / (Math.PI * 2) so the radius is radius = angle * (radiusScale / (Math.PI * 2)) + radiusMin;
Inside the loop we step the angle to match the distance we want to move angle += lineLength / radius; which is derived from the circumference formula (and why we use radians for angles)
Change cxt to the more idiomatic ctx
Add moveTo to move to the start of each circle.
Add ctx.arc to define the circle
When all circles are defined, after the loop draw the path with ctx.stroke()
The code below. As I have only approximated your spiral you can play with the constants to make it fit your needs. Also Note that for the inner spiral, longer line distances will not work as well.
const ctx = myCanvas.getContext("2d");
const centerX = 400;
const centerY = 400;
const markRadius = 2; // radius of each circle mark in pixels
const maxRadius = 350; // 50 pixel boarder
const lineLength = 20; // distance between points in pixels
const radiusScale = 80; // how fast the spiral moves outward per turn
const radiusMin = 10; // start radius
var angle = 0, radius = 0;
ctx.lineWidth = 1;
ctx.strokeStye = "black";
ctx.beginPath();
while (radius < maxRadius) {
radius = angle * (radiusScale / (Math.PI * 2)) + radiusMin;
angle += lineLength / radius;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
ctx.moveTo(x + markRadius, y);
ctx.arc(x, y, markRadius, 0, Math.PI * 2);
}
ctx.stroke();
<canvas id="myCanvas" width="800" height="800" style="border:1px solid #c3c3c3;"></canvas>
If you want separate spirals then it is a minor modification of the above
Define the number of arms in the spiral armCount
Define the rate that the arms turn as the move out "spiralRate"
Just for fun animate the spiralRate
requestAnimationFrame(mainLoop);
const ctx = myCanvas.getContext("2d");
const centerX = 200;
const centerY = 200;
const markRadius = 2; // radius of each circle mark in pixels
const maxRadius = 190; // 50 pixel boarder
const armCount = 8; // Number of arms
const radiusScale = 8; // how fast the spiral moves outward per turn
const radiusMin = 10; // start radius
function drawSpiral(spiralRate) { // spiralRate in pixels per point per turn
var angle = 0, radius = radiusMin;
ctx.lineWidth = 1;
ctx.strokeStye = "black";
ctx.beginPath();
while (radius < maxRadius) {
angle += (Math.PI * 2) / armCount + (spiralRate/ radius);
radius = angle * (radiusScale / (Math.PI * 2)) + radiusMin;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
ctx.moveTo(x + markRadius, y);
ctx.arc(x, y, markRadius, 0, Math.PI * 2);
}
ctx.stroke();
}
function mainLoop(time) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
drawSpiral(Math.sin(time / 4000) * 2); // occilate spiral rate every ~ 24 seconds
requestAnimationFrame(mainLoop);
}
<canvas id="myCanvas" width="400" height="400" style="border:1px solid #c3c3c3;"></canvas>
After changing the code to increment in number form you can see that the "spiral" is not generated in the expected line.
You will ned to work out how many spiral arms you want and calculate that with the stoplimit.
The increment value is also never used.
// var increment = 6/45;
var stoplimit = 51;
https://jsfiddle.net/Anytech/spzyufev/4/
My web project needs to zoom a div element around the mouse position as anchor while mouse wheeling, I was inspired by #Tatarize 's answer at Zoom in on a point (using scale and translate), but I can't implement it exactly, it can't zoom and translate around the mouse position, can any one help?
window.onload = function() {
const STEP = 0.05;
const MAX_SCALE = 10;
const MIN_SCALE = 0.01;
const red = document.getElementById('red');
const yellow = red.parentNode;
let scale = 1;
yellow.onmousewheel = function (event) {
event.preventDefault();
let mouseX = event.clientX - yellow.offsetLeft - red.offsetLeft;
let mouseY = event.clientY - yellow.offsetTop - red.offsetTop;
const factor = event.wheelDelta / 120;
const oldScale = scale;
scale = scale + STEP * factor;
scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale));
const scaleChanged = scale - oldScale;
const offsetX = -(mouseX * scaleChanged);
const offsetY = -(mouseY * scaleChanged);
console.log(offsetX, offsetY);
red.style.transform = 'translate(' + offsetX + 'px, ' + offsetY + 'px)' + 'scale(' + scale + ')';
}
}
.yellow {
background-color: yellow;
width: 400px;
height: 200px;
}
.red {
background-color: red;
width: 100px;
height: 50px;
}
<div class="yellow">
<div id="red" class="red"></div>
</div>
Really incredible, I actually did it.
window.onload = () => {
const STEP = 0.99;
const MAX_SCALE = 5;
const MIN_SCALE = 0.01;
const red = document.getElementById("red");
const yellow = red.parentNode;
let scale = 1;
const rect = red.getBoundingClientRect();
const originCenterX = rect.x + rect.width / 2;
const originCenterY = rect.y + rect.height / 2;
yellow.onwheel = (event) => {
event.preventDefault();
const factor = event.deltaY;
// If current scale is equal to or greater than MAX_SCALE, but you're still zoom in it, then return;
// If current scale is equal to or smaller than MIN_SCALE, but you're still zoom out it, then return;
// Can not use Math.max and Math.min here, think about it.
if ((scale >= MAX_SCALE && factor < 0) || (scale <= MIN_SCALE && factor > 0)) return;
const scaleChanged = Math.pow(STEP, factor);
scale *= scaleChanged;
const rect = red.getBoundingClientRect();
const currentCenterX = rect.x + rect.width / 2;
const currentCenterY = rect.y + rect.height / 2;
const mousePosToCurrentCenterDistanceX = event.clientX - currentCenterX;
const mousePosToCurrentCenterDistanceY = event.clientY - currentCenterY;
const newCenterX = currentCenterX + mousePosToCurrentCenterDistanceX * (1 - scaleChanged);
const newCenterY = currentCenterY + mousePosToCurrentCenterDistanceY * (1 - scaleChanged);
// All we are doing above is: getting the target center, then calculate the offset from origin center.
const offsetX = newCenterX - originCenterX;
const offsetY = newCenterY - originCenterY;
// !!! Both translate and scale are relative to the original position and scale, not to the current.
red.style.transform = 'translate(' + offsetX + 'px, ' + offsetY + 'px)' + 'scale(' + scale + ')';
}
}
.yellow {
background-color: yellow;
width: 200px;
height: 100px;
margin-left: 50px;
margin-top: 50px;
position: absolute;
}
.red {
background-color: red;
width: 200px;
height: 100px;
position: absolute;
}
<div class="yellow">
<div id="red" class="red"></div>
</div>
.onmousewheel is deprecated. Use .onwheel instead.
Also, onwheel event doesn't have wheelDelta property. Use deltaY.
My code given there is to change the viewbox with regard to a zoom point. You are moving the rectangle based on some math that doesn't fit that situation.
The idea is to pan the zoom box with regard to the change in the scale. You are changing the position and location of a rectangle. Which is to say you need to simulate the new position of the red rectangle as if the yellow rectangle were a viewport. Which means that when we zoom in, we are zooming in at a translateX translateY position of a particular scale factor. We then need to translate the value of the zoom point into the right scene space. Then adjust the position of the red rectangle as if it were in that scene space.
Here's the code with some corrections, though I'm clearly missing a few elements. The big thing is the lack of preservation of the translateX translateY stuff. You overwrite it so it ends up just preserving the zoom and screwing up the translateX, translateY stuff back to zero when it's a relative offset of the viewport.
In functional code, zooming in in the rectangle will make the red rectangle fill the entire scene space.
window.onload = function() {
const STEP = 0.05;
const MAX_SCALE = 10;
const MIN_SCALE = 0.01;
const red = document.getElementById('red');
const yellow = document.getElementById('yellow');
const svgArea = document.getElementById('svgArea');
let viewportTranslateX = 0;
let viewportTranslateY = 0;
let viewportScale = 1;
svgArea.onwheel = function (event) {
event.preventDefault();
console.log("mouse coords", event.clientX, event.clientY);
let zoompointX = (event.clientX + (viewportTranslateX / viewportScale)) * viewportScale;
let zoompointY = (event.clientY + (viewportTranslateY / viewportScale)) * viewportScale;
console.log("zoom point prezoom", zoompointX, zoompointY);
const factor = event.deltaY / 120;
const oldScale = viewportScale;
viewportScale = viewportScale * (1 + STEP * factor);
viewportScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, viewportScale));
const scaleChanged = viewportScale - oldScale;
const offsetX = -(zoompointX * scaleChanged);
const offsetY = -(zoompointY * scaleChanged);
console.log("scale", scaleChanged, offsetX, offsetY);
viewportTranslateX += offsetX;
viewportTranslateY += offsetY;
zoompointX = (event.clientX + (viewportTranslateX / viewportScale)) * viewportScale;
zoompointY = (event.clientY + (viewportTranslateY / viewportScale)) * viewportScale;
console.log("zoompoint postzoom", zoompointX, zoompointY);
var x = viewportTranslateX;
var y = viewportTranslateY;
var width = (svgArea.getAttribute("width") * viewportScale);
var height = (svgArea.getAttribute("height") * viewportScale);
svgArea.setAttribute("viewBox", x + " " + y + " " + width + " " + height);
console.log("viewport", x, y, width, height, viewportScale);
}
}
<svg id="svgArea" width=400 height=200 viewBox="0,0,400,200">
<rect id="yellow" width=400 height=200 fill="yellow"/>
<rect id="red" width=100 height=50 fill="red"/>
</svg>
I am looking for the algorithm that takes 3D coordinates and change them to 2D coordinates.
I tried the steps form this Wikipedia Page : https://en.wikipedia.org/wiki/3D_projection#Perspective_projection
and my code so far is this :
var WIDTH = 1280/2;
var HEIGHT = 720/2;
// Distance from center of Canvas (Camera) with a Field of View of 90 digress, to the Canvas
var disToCanvas = Math.tan(45) * WIDTH/2;
var canvas = document.createElement('canvas');
canvas.width = WIDTH;
canvas.height = HEIGHT;
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
var Player = function (){ // Camera
// Camera Coordinates
this.x = 0;
this.y = 0;
this.z = 0;
// Camera Rotation (Angle)
this.rx = 0;
this.ry = 90;
this.rz = 0;
};
var player = new Player();
var Point = function (x, y ,z){
// Point 3D Coordinates
this.x = x;
this.y = y;
this.z = z;
// Point 2D Coordinates
this.X2d = 0;
this.Y2d = 0;
// The function that changes 3D coordinates to 2D
this.update = function (){
var X = (player.x - this.x);
var Y = (player.y - this.y);
var Z = (player.z - this.z);
var Cx = Math.cos(player.rx); // cos(θx)
var Cy = Math.cos(player.ry); // cos(θy)
var Cz = Math.cos(player.rz); // cos(θz)
var Sx = Math.sin(player.rx); // sin(θx)
var Sy = Math.sin(player.ry); // sin(θy)
var Sz = Math.sin(player.rz); // sin(θz)
var Dx = Cy * (Sy*Y + Cz*X) - Sy*Z;
var Dy = Sx * (Cy*Z + Sy * (Sz*Y + Cz*X)) + Cx * (Cy*Y + Sz*X);
var Dz = Cx * (Cy*Z + Sy * (Sz*Y + Cz*X)) - Sx * (Cy*Y + Sz*X);
var Ex = this.x / this.z * disToCanvas; // This isn't 100% correct
var Ey = this.y / this.z * disToCanvas; // This isn't 100% correct
var Ez = disToCanvas; // This isn't 100% correct
this.X2d = Ez/Dz * Dx - Ex + WIDTH/2; // Adding WIDTH/2 to center the camera
this.Y2d = Ez/Dz * Dy - Ez + HEIGHT/2; // Adding HEIGHT/2 to center the camera
}
}
// CREATING, UPDATING AND RENDERING A SQUARE
var point = [];
point[0] = new Point(10, 10, 10);
point[1] = new Point(20, 10, 10);
point[2] = new Point(20, 20, 10);
point[3] = new Point(10, 20, 10);
var run = setInterval(function (){
for (key in point){
point[key].update();
}
ctx.beginPath();
ctx.moveTo(point[0].X2d, point[0].Y2d);
ctx.lineTo(point[1].X2d, point[1].Y2d);
ctx.lineTo(point[2].X2d, point[2].Y2d);
ctx.lineTo(point[3].X2d, point[3].Y2d);
ctx.lineTo(point[0].X2d, point[0].Y2d);
}, 1000/30);
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background: rgba(73,72,62,.99);
}
canvas {
position: absolute;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
outline: 1px solid white;
}
<html>
<head>
</head>
<body>
</body>
</html>
I want a function that can translate 3D to 2D depending on both position and rotation of the camera.
Taking a look at the Wikipedia page you linked it appears that you have errors in your equations for D. It should be:
var Dx = Cy * (Sz*Y + Cz*X) - Sy*Z;
var Dy = Sx * (Cy*Z + Sy * (Sz*Y + Cz*X)) + Cx * (Cz*Y + Sz*X);
var Dz = Cx * (Cy*Z + Sy * (Sz*Y + Cz*X)) - Sx * (Cz*Y + Sz*X);
Also I think you are using the wrong coordinates for E, which is "the viewer's position relative to the display surface" and should not depend on the coordinates of the point.
The y coordinate of your 2D position appears to contain an error too; you use Ez instead of Ey.
Additionally i can recommend this site. It is written for C++ and OpenGL, but it contains a lot of good explanations and diagrams to get a better understanding of what it is you are trying to do.
I have been able to draw the sin wave in horizontal direction as shows the image(image link: https://i.stack.imgur.com/RTpDY.png) and in the vertical direction.
now I need to draw it in an angled 45° direction
could any one help me pleese!
the script code:
var c =document.getElementById("c");
var ctx=c.getContext('2d');
var x=0,y=250,vx=0.05,vy=0.05,a=1;
for(var i=0; i<501;i++){
x += a;
y = Math.floor(500 * (0.5 - 0.15 * Math.sin(vy)));
vy += vx;
// this.ctx.clearRect(0, 0, 500,500);
this.ctx.beginPath();
this.ctx.arc(x, y, 2, 0, Math.PI * 2, true);
this.ctx.closePath();
this.ctx.fillStyle = 'red';
this.ctx.fill();
console.log("x:"+x+"y:"+y+"vy:"+vy);
}
Draw a sin wave along a line
The following will draw a sin wave aligned to a line. The line can be in any direction.
The standard settings
The wave length will be in pixels. For a sin wave to make a complete cycle you need to rotate its input angle by Math.PI * 2 so you will need to convert it to value matching pixel wavelength.
const waveLen = 400; // pixels
The phase of the sin wave is at what part of the wave it starts at, as the wave length is in pixels, the phase is also in pixels, and represents the distance along the wave that denotes the starting angle.
const phase = 200; // mid way
The amplitude of the wave is how far above and below the center line the wave max and min points are. This is again in pixels
const amplitude = 100;
A wave also has an offset, though in this case not really important I will added it as well. In pixels as well
const offset = 0;
The line that marks the center of the wave has a start and end coordinate
const x1 = 20;
const y1 = 20;
const x2 = 400;
const y2 = 400;
And some context settings
const lineWidth = 3;
const lineCap = "round";
const lineJoin = "round";
const strokeStyle = "blue";
Example code
And so to the code that does the rendering, I have expanded the code with comments so you can read what is going on. Below that is a more useable version.
const ctx = canvas.getContext("2d");
canvas.width = innerWidth;
canvas.height = innerHeight;
window.addEventListener("resize", () => {
canvas.width = innerWidth;
canvas.height = innerHeight;
y2 = x2 = innerWidth; // at 45 deg
drawSinWave();
})
const waveLen = 120; // pixels
const phase = 50; // mid way
const amplitude = 25;
const offset = 0;
const x1 = 20;
const y1 = 20;
var x2 = 400; // as vars to let it change to fit resize
var y2 = 400;
function drawSinWave() {
ctx.lineWidth = 3;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.strokeStyle = "blue";
// get the vector form of the line
const vx = x2 - x1;
const vy = y2 - y1;
// Get the length of the line in pixels
const dist = Math.sqrt(vx * vx + vy * vy);
// Make the vector one pixel long to move along the line
const px = vx / dist;
const py = vy / dist;
// We also need a vector to move out from the line (at 90 deg to the ine)
// So rotate the pixel vector 90deg CW
const ax = -py; // a for amplitude vector
const ay = px;
// Begin the path
ctx.beginPath();
// Now loop along every pixel in the line
// We go past the end a bit as floating point errors can cause it to end
// a pixels too early
for (var i = 0; i <= dist + 0.5; i++) {
// fix i if past end
if (i > dist) {
i = dist
} // Carefull dont mess with this ot it will block the page
// Use the distance to get the current angle of the wave
// based on the wave length and phase
const ang = ((i + phase) / waveLen) * Math.PI * 2;
// and at this position get sin
const val = Math.sin(ang);
// Scale to match the amplitude and move to offset
// as the distance from the center of the line
const amp = val * amplitude + offset;
// Get line ceneter at distance i using the pixel vector
var x = x1 + px * i;
var y = y1 + py * i;
// Use the amp vector to move away from the line at 90 degree
x += ax * amp;
y += ay * amp;
// Now add the point
ctx.lineTo(x, y);
}
ctx.stroke();
}
drawSinWave();
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id=canvas width=4 00 height=4 00></canvas>
As a more useable function with a few shortcuts
const ctx = canvas.getContext("2d");
canvas.width = innerWidth;
canvas.height = innerHeight;
window.addEventListener("resize", () => {
canvas.width = innerWidth;
canvas.height = innerHeight;
waveExample.y2 = waveExample.x2 = innerWidth; // at 45 deg
drawSinWave(waveExample);
})
const waveExample = {
waveLen: 100, // pixels
phase: 50, // mid way
amplitude: 35,
offset: 0,
x1: 20,
y1: 20,
x2: 400, // as vars to let it change to fit resize
y2: 400,
lineWidth : 5,
lineCap : "round",
lineJoin : "round",
strokeStyle : "Red",
}
function drawSinWave(wave) {
ctx.lineWidth = wave.lineWidth;
ctx.lineCap = wave.lineCap;
ctx.lineJoin = wave.lineJoin;
ctx.strokeStyle = wave.strokeStyle;
var vx = wave.x2 - wave.x1;
var vy = wave.y2 - wave.y1;
const dist = Math.sqrt(vx * vx + vy * vy);
vx /= dist;
vy /= dist;
ctx.beginPath();
for (var i = 0; i <= dist + 0.5; i++) {
if (i > dist) { i = dist }
const pos = Math.sin(((i + wave.phase) / wave.waveLen) * Math.PI * 2) * wave.amplitude + wave.offset;
ctx.lineTo(
wave.x1 + vx * i - vy * pos,
wave.y1 + vy * i + vx * pos
);
}
ctx.stroke();
}
drawSinWave(waveExample);
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id=canvas width=4 00 height=4 00></canvas>
The easiest solution is rotating the canvas:
ctx.rotate(45*Math.PI/180);
Although I'm presuming you need the canvas orientation fixed and you need to mathematically alter the way you draw? In which case here's a whole bunch of math about how to plot sine waves at a rotated counterclockwise:
http://mathman.biz/html/rotatingsine.html
I would like to generate a canvas image using gradients in some clever way. I would like the image to looks something like this:
I just can't get my head around it. I need to generate lines in the form and arc - or use gradients with color stops in some clever way. Maybe it would be a lot easier if I converted to HSL and just go through the HUE values?
For example in a rectangle format I could
for (var i = 0; i < h; ++i) {
var ratio = i/h;
var hue = Math.floor(360*ratio);
var sat = 100;
var lum = 50;
line(dc, hslColor(hue,sat,lum), left_margin, top_margin+i, left_margin+w, top_margin+i);
}
Does anybody have any clever tips on how to produce this image using canvas?
This is not perfect (due to drawing steps ...), but it can help you :
http://jsfiddle.net/afkLY/2/
HTML:
<canvas id="colors" width="200" height="200"></canvas>
Javascript:
var canvas = document.getElementById("colors");
var graphics = canvas.getContext("2d");
var CX = canvas.width / 2,
CY = canvas.height/ 2,
sx = CX,
sy = CY;
for(var i = 0; i < 360; i+=0.1){
var rad = i * (2*Math.PI) / 360;
graphics.strokeStyle = "hsla("+i+", 100%, 50%, 1.0)";
graphics.beginPath();
graphics.moveTo(CX, CY);
graphics.lineTo(CX + sx * Math.cos(rad), CY + sy * Math.sin(rad));
graphics.stroke();
}
The idea is to draw the disc line by line with a hue value corresponding to the line direction.
You can change the color base rotation by adding a radius angle to rad variable (adding -pi/2 to rad would make the gradient look like your figure).
EDIT:
I made a new demo that generalizes the concept a bit and renders a rainbow polygon. Here is the CodePen.
To get rid of the small voids beteween the colors, I used quads that overflow to the next color part, except for the last one.
Small adjustment to make it have a white center
var canvas = document.getElementById('colorPicker');
var graphics = canvas.getContext("2d");
var CX = canvas.width / 2,
CY = canvas.height / 2,
sx = CX,
sy = CY;
for (var i = 0; i < 360; i += 0.1) {
var rad = i * (2 * Math.PI) / 360;
var grad = graphics.createLinearGradient(CX, CY, CX + sx * Math.cos(rad), CY + sy * Math.sin(rad));
grad.addColorStop(0, "white");
grad.addColorStop(0.01, "white");
grad.addColorStop(0.99, "hsla(" + i + ", 100%, 50%, 1.0)");
grad.addColorStop(1, "hsla(" + i + ", 100%, 50%, 1.0)");
graphics.strokeStyle = grad;
graphics.beginPath();
graphics.moveTo(CX, CY);
graphics.lineTo(CX + sx * Math.cos(rad), CY + sy * Math.sin(rad));
graphics.stroke();
}
Here is an alternate approach that takes a slightly more functional approach:
var canvas = document.getElementById("radial"),
ctx = canvas.getContext("2d"),
width = canvas.width,
height = canvas.height,
center = { x: width/2, y: height/2 },
diameter = Math.min(width, height);
var distanceBetween = function(x1,y1,x2,y2) {
// Get deltas
var deltaX = x2 - x1,
deltaY = y2 - y1;
// Calculate distance from center
return Math.sqrt(deltaX*deltaX+deltaY*deltaY);
}
var angleBetween = function(x1,y1,x2,y2) {
// Get deltas
var deltaX = x2 - x1,
deltaY = y2 - y1;
// Calculate angle
return Math.atan2(deltaY, deltaX);
}
var radiansToDegrees = _.memoize(function(radians) {
// Put in range of [0,2PI)
if (radians < 0) radians += Math.PI * 2;
// convert to degrees
return radians * 180 / Math.PI;
})
// Partial application of center (x,y)
var distanceFromCenter = _.bind(distanceBetween, undefined, center.x, center.y)
var angleFromCenter = _.bind(angleBetween, undefined, center.x, center.y)
// Color formatters
var hslFormatter = function(h,s,l) { return "hsl("+h+","+s+"%,"+l+"%)"; },
fromHue = function(h) { return hslFormatter(h,100,50); };
// (x,y) => color
var getColor = function(x,y) {
// If distance is greater than radius, return black
return (distanceFromCenter(x,y) > diameter/2)
// Return black
? "#000"
// Determine color
: fromHue(radiansToDegrees(angleFromCenter(x,y)));
};
for(var y=0;y<height;y++) {
for(var x=0;x<width;x++) {
ctx.fillStyle = getColor(x,y);
ctx.fillRect( x, y, 1, 1 );
}
}
It uses a function to calculate the color at each pixel – not the most efficient implementation, but perhaps you'll glean something useful from it.
Note it uses underscore for some helper functions like bind() – for partial applications – and memoize.
Codepen for experimentation.