So I have made some waves on a canvas. Now I would also like to add a background to make like a nice sunset with waves. But my problem occurs when I make ctx.fillRect to draw the background. As I need to clear the area around the bottom for the waves to work, the whole screen is being cleared. Therefore also clearing the background
whole code
var c = document.getElementById("screen2");
var ctx = c.getContext("2d");
var cw = c.width = window.innerWidth;
var ch = c.height = window.innerHeight;
var cx = cw / 2,
cy = ch / 2;
var rad = Math.PI / 180;
var w = window.innerWidth;
var h = 100;
var amplitude = h;
var frequency = .01;
var phi = 0;
var frames = 0;
var stopped = true;
var gradientSky = ctx.createLinearGradient(0, ch / 2, 0, ch);
gradientSky.addColorStop(0, "#C1274E");
gradientSky.addColorStop(0.25, "#C344B7");
gradientSky.addColorStop(0.5, "#B2244A");
gradientSky.addColorStop(0.75, "#B2244A");
gradientSky.addColorStop(1, "#B2244A");
ctx.fillStyle = gradientSky;
ctx.fillRect(0, 0, cw, ch);
var gradientA = ctx.createLinearGradient(0, ch / 2, 0, ch);
gradientA.addColorStop(0, "#7a88d9");
gradientA.addColorStop(1, "#5a6bd0");
var gradientB = ctx.createLinearGradient(0, ch / 2, 0, ch);
gradientB.addColorStop(0, "#646593");
gradientB.addColorStop(1, "#0f2460");
ctx.lineWidth = 4;
var step = 0;
function drawWaves() {
frames++;
phi = frames / 88;
ctx.clearRect(0, 0, cw, ch);
ctx.beginPath();
ctx.moveTo(0, ch);
for (var x = 0; x < w; x++) {
y = Math.sin(x * frequency + phi) * amplitude / 2 + amplitude / 2;
//y = Math.cos(x * frequency + phi) * amplitude / 2 + amplitude / 2;
ctx.lineTo(x, y + ch - 390 + Math.sin(step / 2) * 20); // setting it to the bottom of the page 100= lift
}
ctx.lineTo(w, ch);
ctx.lineTo(0, ch);
ctx.fillStyle = gradientA;
ctx.fill();
frames++;
phi = frames / 60;
ctx.beginPath();
ctx.moveTo(0, ch);
for (var x = 0; x < w; x++) {
y = Math.sin(x * frequency + phi) * amplitude / 4 + amplitude / 4;
//y = Math.cos(x * frequency + phi) * amplitude / 2 + amplitude / 2;
ctx.lineTo(x, y + ch - 380 + Math.sin(step) * 20); // setting it to the bottom of the page 100= lift
}
ctx.lineTo(w, ch);
ctx.lineTo(0, ch);
ctx.fillStyle = gradientB;
ctx.fill();
step += 0.02;
requestId = window.requestAnimationFrame(drawWaves);
}
requestId = window.requestAnimationFrame(drawWaves);
I'm pretty sure that has to do what x and y coordinates I inserted into the ctx.fillRect(); and ctx.clearRect();. But I have tried all variants I could think of but still nothing works. Sometimes I get the Background to appear but then the clear tag wont clear the waves properly. Just for better understanding the ch and cw are window.innerHeight and window.innerWidth although this can also be seen in the code.
The trick here is really simple. As most background images, your sunset also should fill the whole screen and will later be covered by the waves and ultimately the bubbles. This is how a painter would draw a painting. Since the background covers the whole canvas, you even don't need to clear it before because this is essentially being done by filling the canvas with the background.
So all you need to change is remove the clearReact() call and replace it by a fillRect(0,0,cw,ch) after setting the fillStyle to gradientSky.
Here's your modified example:
// Author:
// Name:
// URL: https://jsfiddle.net/7z2yh3pg/
function Blasen() {
const section = document.querySelector('#screen')
const createElement = document.createElement('spawn')
var size = Math.random() * 60;
createElement.style.width = 30 + size + 'px';
createElement.style.height = 30 + size + 'px';
createElement.style.left = Math.random() * innerWidth + "px";
section.appendChild(createElement);
setTimeout(() => {
createElement.remove()
}, 600)
}
const Blaseninterval = setInterval(Blasen, 100)
var c = document.getElementById("screen2");
var ctx = c.getContext("2d");
var cw = c.width = window.innerWidth;
var ch = c.height = window.innerHeight;
var cx = cw / 2,
cy = ch / 2;
var rad = Math.PI / 180;
var w = window.innerWidth;
var h = 100;
var amplitude = h;
var frequency = .01;
var phi = 0;
var frames = 0;
var stopped = true;
var gradientSky = ctx.createLinearGradient(0, 0, 0, ch);
gradientSky.addColorStop(0, "#C1274E");
gradientSky.addColorStop(0.25, "#C344B7");
gradientSky.addColorStop(0.5, "#B2244A");
gradientSky.addColorStop(0.75, "#B2244A");
gradientSky.addColorStop(1, "#B2244A");
ctx.fillStyle = gradientSky;
ctx.fill();
var gradientA = ctx.createLinearGradient(0, ch / 2, 0, ch);
gradientA.addColorStop(0, "#7a88d9");
gradientA.addColorStop(1, "#5a6bd0");
var gradientB = ctx.createLinearGradient(0, ch / 2, 0, ch);
gradientB.addColorStop(0, "#646593");
gradientB.addColorStop(1, "#0f2460");
ctx.lineWidth = 4;
var step = 0;
function drawWaves() {
frames++;
phi = frames / 88;
ctx.fillStyle = gradientSky;
ctx.beginPath();
ctx.rect(0, 0, cw, ch);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(0, ch);
for (var x = 0; x < w; x++) {
y = Math.sin(x * frequency + phi) * amplitude / 2 + amplitude / 2;
//y = Math.cos(x * frequency + phi) * amplitude / 2 + amplitude / 2;
ctx.lineTo(x, y + ch - 250 + Math.sin(step / 2) * 20); // setting it to the bottom of the page 100= lift
}
ctx.lineTo(w, ch);
ctx.lineTo(0, ch);
ctx.fillStyle = gradientA;
ctx.fill();
frames++;
phi = frames / 60;
ctx.beginPath();
ctx.moveTo(0, ch);
for (var x = 0; x < w; x++) {
y = Math.sin(x * frequency + phi) * amplitude / 4 + amplitude / 4;
//y = Math.cos(x * frequency + phi) * amplitude / 2 + amplitude / 2;
ctx.lineTo(x, y + ch - 240 + Math.sin(step) * 20); // setting it to the bottom of the page 100= lift
}
ctx.lineTo(w, ch);
ctx.lineTo(0, ch);
ctx.fillStyle = gradientB;
ctx.fill();
step += 0.02;
requestId = window.requestAnimationFrame(drawWaves);
}
requestId = window.requestAnimationFrame(drawWaves);
body,
html {
margin: 0;
padding: 0;
overflow: hidden;
}
canvas {
display: block;
margin: 0 auto;
width: 100%;
height: 100vh;
}
#screen {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
#screen spawn {
position: absolute;
bottom: -80px;
background: transparent;
border-radius: 50%;
pointer-events: none;
box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.5);
animation: animate 3s linear infinite;
}
#screen spawn:before {
content: '';
position: absolute;
width: 100%;
height: 100%;
transform: scale(0.25) translate(-70%, -70%);
background: radial-gradient(#fff, transparent);
opacity: 0.6;
border-radius: 50%;
}
#keyframes animate {
0% {
transform: translateY(0%);
opacity: 1;
}
99% {
opacity: 1;
}
100% {
transform: translateY(-2000%);
opacity: 0;
}
}
<canvas id="screen2"></canvas>
<div id="screen"></div>
Related
So I have wanted to make an ocean in the night with a canvas. I got the Wave part down but I want them to be filled. So that they are not just a lign anymore but a solid block locking like waves. I really like how they look right now but if you have any tweeks that could make them look even better feel free to share them!
Thanks for the Help in Advance!
var c = document.getElementById("screen2");
var ctx = c.getContext("2d");
var cw = c.width = window.innerWidth;
var ch = c.height = window.innerHeight;
var cx = cw / 2,
cy = ch / 2;
var rad = Math.PI / 180;
var w = window.innerWidth;
var h = 100;
var amplitude = h;
var frequency = .01;
var phi = 0;
var frames = 0;
var stopped = true;
//ctx.strokeStyle = "Cornsilk";
ctx.lineWidth = 4;
//first wave
function Draw() {
frames++
phi = frames / 88;
ctx.clearRect(0, 0, cw, ch);
ctx.beginPath();
ctx.moveTo(0, ch);
for (var x = 0; x < w; x++) {
y = Math.sin(x * frequency + phi) * amplitude/2 + amplitude /2;
//y = Math.cos(x * frequency + phi) * amplitude / 2 + amplitude / 2;
ctx.lineTo(x, y + ch -110); // setting it to the bottom of the page 100= lift
}
ctx.lineTo(w, ch);
ctx.lineTo(0, ch);
ctx.stroke();
requestId = window.requestAnimationFrame(Draw);
}
requestId = window.requestAnimationFrame(Draw);
//second wave
function Draw2() {
frames++
phi = frames / 72 ;
ctx.beginPath();
ctx.moveTo(0, ch);
for (var x = 0; x < w; x++) {
y = Math.sin(x * frequency + phi) * amplitude/4 + amplitude/4;
//y = Math.cos(x * frequency + phi) * amplitude / 2 + amplitude / 2;
ctx.lineTo(x, y + ch -110); // setting it to the bottom of the page 100= lift
}
ctx.lineTo(w, ch);
ctx.lineTo(0, ch);
ctx.stroke();
requestId = window.requestAnimationFrame(Draw2);
}
requestId = window.requestAnimationFrame(Draw2);
canvas {
display:block;
margin:0 auto;
width:100%;
height:100vh;
}
<canvas id="screen2"></canvas>
First and foremost what I really would change is using requestAnimationFrame two times as it's completely redundant. You can group your drawing operations in a single function and just execute this.
As #mousetail hinted a simple 'fix' would be using .fill() instead of .stroke(). The problem is that you would have two completely solid shapes of the same colour making it look like there's just a single wave. So the next step would be adding two different colours. As that might still look a little boring I'd use two linear gradients - a lighter for the foreground and a darker for the background wave.
To make it look even more interesting I'd move the waves up & down over time.
var c = document.getElementById("screen2");
var ctx = c.getContext("2d");
var cw = c.width = window.innerWidth;
var ch = c.height = window.innerHeight;
var cx = cw / 2,
cy = ch / 2;
var rad = Math.PI / 180;
var w = window.innerWidth;
var h = 100;
var amplitude = h;
var frequency = .01;
var phi = 0;
var frames = 0;
var stopped = true;
var gradientA = ctx.createLinearGradient(0, ch / 2, 0, ch);
gradientA.addColorStop(0, "#29c3d3");
gradientA.addColorStop(1, "#15656e");
var gradientB = ctx.createLinearGradient(0, ch / 2, 0, ch);
gradientB.addColorStop(0, "#55dee5");
gradientB.addColorStop(1, "#399499");
ctx.lineWidth = 4;
var step = 0;
function drawWaves() {
frames++;
phi = frames / 88;
ctx.clearRect(0, 0, cw, ch);
ctx.beginPath();
ctx.moveTo(0, ch);
for (var x = 0; x < w; x++) {
y = Math.sin(x * frequency + phi) * amplitude / 2 + amplitude / 2;
//y = Math.cos(x * frequency + phi) * amplitude / 2 + amplitude / 2;
ctx.lineTo(x, y + ch - 110 + Math.sin(step / 2) * 10); // setting it to the bottom of the page 100= lift
}
ctx.lineTo(w, ch);
ctx.lineTo(0, ch);
ctx.fillStyle = gradientA;
ctx.fill();
frames++;
phi = frames / 72;
ctx.beginPath();
ctx.moveTo(0, ch);
for (var x = 0; x < w; x++) {
y = Math.sin(x * frequency + phi) * amplitude / 4 + amplitude / 4;
//y = Math.cos(x * frequency + phi) * amplitude / 2 + amplitude / 2;
ctx.lineTo(x, y + ch - 110 + Math.sin(step) * 20); // setting it to the bottom of the page 100= lift
}
ctx.lineTo(w, ch);
ctx.lineTo(0, ch);
ctx.fillStyle = gradientB;
ctx.fill();
step += 0.02;
requestId = window.requestAnimationFrame(drawWaves);
}
requestId = window.requestAnimationFrame(drawWaves);
canvas {
display: block;
margin: 0 auto;
width: 100%;
height: 100vh;
}
<canvas id="screen2"></canvas>
I'm making a menu for my Sudoku game to choose numbers for selected cells.
I managed to do a circular animated menu with X canvas elements each representing one number (+ one is empty). Then I rotate all these elements with CSS transform to create a perfect circle.
And here the problem appears - there are ugly transparent strokes between each element that I can't manage to remove.
This is how I draw it:
My app is a bit complicated but I managed to create a demo:
let parent = document.getElementsByClassName('menu')[0];
let elSize = parent.getBoundingClientRect().width;
let upscale = 2;
let total = 10;
let length = elSize / 2;
for (let i = 0; i < total; i++) {
// create new canvas
let val = document.createElement('canvas');
val.classList.add("value");
let deg = 360 / total;
//set sizes and rotation
val.height = length * upscale;
val.width = elSize * upscale;
val.style.width = elSize + "px";
val.style.height = length + "px";
val.style.setProperty("--rotation", (i / total * 360) + "deg");
// get context
let ctx = val.getContext("2d");
ctx.fillStyle = "blue";
ctx.imageSmoothingEnabled = true;
// full circle center
let center = {
x: length * upscale,
y: length * upscale
}
//function to fill the circle part (step 1 and 2 on the image)
const fillWedge = (cx, cy, radius, startAngle, endAngle, fillcolor, stroke = false) => {
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, radius, startAngle, endAngle);
ctx.closePath();
if (stroke) {
ctx.lineWidth = 1;
ctx.strokeStyle = fillcolor;
ctx.stroke();
} else {
ctx.fillStyle = fillcolor;
ctx.fill();
}
}
const degToAngle = (deg) => {
let start = -Math.PI / 2;
let fullCircle = Math.PI * 2;
return (start + fullCircle * (deg / 360));
}
ctx.save();
ctx.imageSmoothingEnabled = false;
ctx.globalCompositeOperation = "source-out"; {
//smaller circle
let cx = center.x;
let cy = center.y;
let radius = length * upscale / 3;
let startAngle = -((deg + 1) / 2) % 360;
let endAngle = ((deg + 1) / 2) % 360;
fillWedge(cx, cy, radius, degToAngle(startAngle), degToAngle(endAngle), ctx.fillStyle);
}
//make it semi-transparent
ctx.globalAlpha = 0.8; {
//bigger circle
let cx = center.x;
let cy = center.y;
let radius = length * upscale;
let startAngle = -(deg / 2) % 360;
let endAngle = (deg / 2) % 360;
fillWedge(cx, cy, radius, degToAngle(startAngle), degToAngle(endAngle), ctx.fillStyle);
}
ctx.restore();
//draw text
if (i !== 0) {
ctx.save();
ctx.translate(length * upscale, length / 3 * upscale);
ctx.rotate(-(i / total * 360) / 180 * Math.PI);
ctx.font = "600 " + (18 * upscale) + 'px Consolas';
ctx.textAlign = "center";
ctx.fillStyle = "white";
ctx.fillText((i) + "", 0, 5 * upscale);
ctx.restore()
}
//add element to menu
parent.appendChild(val);
}
html {
background: url(https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fcdn.cienradios.com%2Fwp-content%2Fuploads%2Fsites%2F13%2F2020%2F06%2FShrek-portada.jpg&f=1&nofb=1) no-repeat center;
display: flex;
justify-content: center;
align-content: center;
}
.menu {
width: 400px;
aspect-ratio: 1;
position: relative;
border-radius: 50%;
}
.menu .value {
--rotation: 0deg;
position: absolute;
top: 0;
bottom: 50%;
left: 50%;
transform-origin: bottom;
transform: translate(-50%, 0) rotate(var(--rotation));
}
p {
color: white;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sudoku Test</title>
</head>
<body>
<div class="menu">
</div>
<p>
See, it's semi-transparent and you can clearly see lines between elements
<br>
<b>How to fix it?!</b>
</p>
</body>
</html>
Any way to remove those spaces? I know it's a bit too much detailed question, but I really can't manage to fix it. Thanks.
These are caused by antialiasing. Since you do draw on diagonals, the shape doesn't fall on full pixel boundaries. So to smooth the lines, the browser will make those pixels that should have been painted only partially, more transparent. When stacked these transparent pixels won't add up exactly to full opacity (0.5 opacity + 0.5 opacity = 0.75 opacity, not 1). So you'll see these lines.
Removing the smoothing here won't help, because the alternative would be to fill in a marching-square fashion, but that would result in either complete holes in some places, either in overlapping pixels, which would be visible since your shapes aren't fully opaque.
Usually the cheap trick for that issue is to stroke a couple of pixels around the shape in the same color as it's filled. But once again since your shapes are filled with semi-transparent colors this trick won't do it.
You could hack something around by drawing all your shapes at full opacity, and applying the transparency on a common container. But this means that your texts would need their own canvas, and their own container (otherwise they'd be transparent too).
let parent = document.getElementsByClassName('menu')[0];
const textsParent = document.getElementsByClassName('texts')[0];
let elSize = parent.getBoundingClientRect().width;
let upscale = 2;
let total = 10;
let length = elSize / 2;
for (let i = 0; i < total; i++) {
// create new canvas
let val = document.createElement('canvas');
val.classList.add("value");
let deg = 360 / total;
//set sizes and rotation
val.height = length * upscale;
val.width = elSize * upscale;
val.style.width = elSize + "px";
val.style.height = length + "px";
val.style.setProperty("--rotation", (i / total * 360) + "deg");
// get context
let ctx = val.getContext("2d");
ctx.fillStyle = "blue";
// full circle center
let center = {
x: length * upscale,
y: length * upscale
}
//function to fill the circle part (step 1 and 2 on the image)
const fillWedge = (cx, cy, radius, startAngle, endAngle, fillcolor, stroke = false) => {
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, radius, startAngle, endAngle);
ctx.closePath();
ctx.lineWidth = 2;
ctx.strokeStyle = fillcolor;
ctx.stroke();
ctx.fillStyle = fillcolor;
ctx.fill();
}
const degToAngle = (deg) => {
let start = -Math.PI / 2;
let fullCircle = Math.PI * 2;
return (start + fullCircle * (deg / 360));
}
ctx.save();
ctx.imageSmoothingEnabled = false;
{
//smaller circle
let cx = center.x;
let cy = center.y;
let radius = length * upscale / 3;
let startAngle = -((deg + 1) / 2) % 360;
let endAngle = ((deg + 1) / 2) % 360;
fillWedge(cx, cy, radius, degToAngle(startAngle), degToAngle(endAngle), ctx.fillStyle);
}
{
//bigger circle
let cx = center.x;
let cy = center.y;
let radius = length * upscale;
let startAngle = -(deg / 2) % 360;
let endAngle = (deg / 2) % 360;
fillWedge(cx, cy, radius, degToAngle(startAngle), degToAngle(endAngle), ctx.fillStyle);
}
ctx.restore();
//draw text
if (i !== 0) {
// we need a new canvas just for the text
const val2 = val.cloneNode();
const ctx = val2.getContext("2d");
ctx.save();
ctx.translate(length * upscale, length / 3 * upscale);
ctx.rotate(-(i / total * 360) / 180 * Math.PI);
ctx.font = "600 " + (18 * upscale) + 'px Consolas';
ctx.textAlign = "center";
ctx.fillStyle = "white";
ctx.fillText((i) + "", 0, 5 * upscale);
ctx.restore()
textsParent.appendChild(val2);
}
//add element to menu
parent.appendChild(val);
}
html {
background: url(https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fcdn.cienradios.com%2Fwp-content%2Fuploads%2Fsites%2F13%2F2020%2F06%2FShrek-portada.jpg&f=1&nofb=1) no-repeat center;
display: flex;
justify-content: center;
align-content: center;
}
.menu, .texts {
width: 400px;
aspect-ratio: 1;
position: relative;
border-radius: 50%;
}
.menu .value, .texts canvas {
--rotation: 0deg;
position: absolute;
top: 0;
bottom: 50%;
left: 50%;
transform-origin: bottom;
transform: translate(-50%, 0) rotate(var(--rotation));
}
.menu { opacity: 0.8 }
.text-container {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-content: center;
}
<div class="menu">
</div>
<div class="text-container">
<div class="texts">
</div>
</div>
So instead the best is probably to refactor your code entirely to use a single canvas instead. If you tell the browser to draw all your shapes in a single subpath, it will be able to place the tracing perfectly where it should be and will be able to draw all this without any line in between:
const canvas = document.querySelector("canvas");
canvas.width = canvas.height = 500;
const ctx = canvas.getContext("2d");
const parts = 10;
const theta = Math.PI / (parts / 2);
const trace = (cx, cy, r1, r2, t) => {
ctx.moveTo(cx + r2, cy);
ctx.lineTo(cx + r1, cy);
ctx.arc(cx, cy, r1, 0, t);
ctx.arc(cx, cy, r2, t, 0, true);
};
ctx.translate(250, 250);
ctx.rotate(-Math.PI / 2 - theta);
// draw all but the last part
for (let i = 0; i < parts - 1; i++) {
ctx.rotate(theta);
trace(0, 0, 50, 200, theta);
}
ctx.globalAlpha = 0.8;
ctx.fillStyle = "blue";
ctx.fill(); // in a single pass
// draw the last part in red
ctx.fillStyle = "red";
ctx.rotate(theta);
ctx.beginPath();
trace(0, 0, 50, 200, theta);
ctx.fill();
canvas {
/* checkered effect from https://stackoverflow.com/a/51054396/3702797 */
--tint:rgba(255,255,255,0.9);background-image:linear-gradient(to right,var(--tint),var(--tint)),linear-gradient(to right,black 50%,white 50%),linear-gradient(to bottom,black 50%,white 50%);background-blend-mode:normal,difference,normal;background-size:2em 2em;
}
<canvas></canvas>
I would like to rotate an image drawn to Canvas with Javascript, but without using the Context's rotate method (or any JS library). The reason is because this is demonstrating an issue I am having in another language where I do not have access to these.
I have created a first draft (below), but I have two issues with my implementation: the pixel-by-pixel rotation of the copied bitmap is extremely slow and it is leaving gaps between the pixels.
Is there a faster method for putting the bitmap data at an angle that would not leave the gaps? Please let me know if you have any questions. Thank you.
const c1 = document.getElementById("c1");
const c2 = document.getElementById("c2");
const slider = document.getElementById('slider');
const removeAllChildNodes = (parent) =>
{
while (parent.firstChild) {
parent.removeChild(parent.firstChild);
}
}
const rotatedBoundingBox = (width,height,rotation) =>
{
let rot_w = Math.abs(width * Math.cos(rotation)) + Math.abs(height * Math.sin(rotation));
let rot_h = Math.abs(width * Math.sin(rotation)) + Math.abs(height * Math.cos(rotation));
return {width:rot_w,height:rot_h};
}
const render = (rotation) => {
const canvas = document.createElement("canvas");
const canvas2 = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const ctx2 = canvas2.getContext("2d");
let txt = "Hello World";
ctx.font = '20px sans-serif';
let metrics = ctx.measureText(txt);
canvas.width = metrics.width;
canvas.height = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
ctx.fillStyle = "#636674";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = '20px sans-serif';
ctx.textAlign = "center";
ctx.fillStyle = "#ffaefa";
ctx.fillText(txt, canvas.width/2, canvas.height);
const { width, height } = rotatedBoundingBox(canvas.width,canvas.height,rotation);
canvas2.width = width;
canvas2.height = height;
cos = Math.cos(-rotation);
sin = Math.sin(-rotation);
cx = canvas.width/2;
cy = canvas.height/2;
for (x = 0; x < canvas.width; x++)
{
for (y = 0; y < canvas.height; y++)
{
var imgd = ctx.getImageData(x, y, 1, 1);
var pix = imgd.data;
var red = pix[0];
var green = pix[1];
var blue = pix[2];
var alpha = pix[3];
nx = ((cos * (x-cx)) + (sin * (y - cy))+canvas2.width/2);
ny = ((cos * (y-cy)) - (sin * (x - cx))+canvas2.height/2);
ctx2.putImageData(imgd, nx, ny);
}
}
removeAllChildNodes(c1);
removeAllChildNodes(c2);
c1.appendChild(canvas);
c2.appendChild(canvas2);
}
slider.addEventListener('input', (e) => {
document.getElementById('currentDegree').innerHTML = e.target.value;
const rad = parseInt(e.target.value) * Math.PI / 180;
render(rad);
})
document.getElementById('currentDegree').innerHTML = slider.value;
render(parseInt(slider.value) * Math.PI / 180);
canvas
{
border: 1px solid rgba(0,0,0,0.2);
}
main
{
display: flex;
}
.contain
{
display: inline-block;
}
<div>
<input id="slider" type="range" min="0" max="360" value="66"/>
<span>rotation: <span id="currentDegree">0</span>°</span>
</div>
<main>
<div id="c1" class="contain">
</div>
<div id="c2" class="contain">
</div>
</main>
Scanline 2D image render.
To remove holes in the render scan each pixel you are rendering to and calculate where on that image that pixel is from.
There is a slight cost in that you end up scanning pixels that have no content however that can be resolved if you use a scanline polygon render
Simple example
The example below creates an image, gets the pixels of the image and also creates a buffer to hold rendered pixels.
Rather than read and write pixels per channel, it creates a Uint32Array view of the buffers so all 4 pixel channels can be read and written in one operation.
Because the image is uniform the scan lines can be optimized such that the full transform need only be calculated once per row.
The scanline function takes 6 arguments. ox, oy The rotation origin, ang the rotation in radians, scale the scale to render the image, r32 Uint32Array view of image Data contains the image to draw. w32 Uint32Array view of image data to hold the resulting render.
It is reasonably performant, with the example rendering about 160,000 pixels per update.
const ctx = canvas.getContext("2d");
const W = ctx.canvas.width, H = ctx.canvas.height;
createTestImage();
const pxWrite = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const w32 = new Uint32Array(pxWrite.data.buffer); /* not needed for 8 bit ver */
const pxRead = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const r32 = new Uint32Array(pxRead.data.buffer); /* not needed for 8 bit ver */
function rotate() {
const ang = angSlider.value * (Math.PI / 180);
const scale = scaleSlider.value / 100;
/* For 8bit replace the following line with the commented line below it */
scanLine(W / 2, H / 2, ang, scale, r32, w32);
// scanLine8Bit(W / 2, H / 2, ang, scale, pxRead.data, pxWrite.data);
ctx.putImageData(pxWrite,0,0);
}
function scanLine(ox, oy, ang, scale, r32, w32) {
const xAx = Math.cos(ang) / scale;
const xAy = Math.sin(ang) / scale;
w32.fill(0);
var rx, ry, idxW, x = 0, y = 0;
while (y < H) {
const xx = x - ox, yy = y - oy;
rx = xx * xAx - yy * xAy + ox; // Get image coords for row start
ry = xx * xAy + yy * xAx + oy;
idxW = y * W + x;
while (x < W) {
if (rx >= 0 && rx < W && ry >= 0 && ry < H) {
w32[idxW] = r32[(ry | 0) * W + (rx | 0)];
}
idxW ++;
rx += xAx;
ry += xAy;
x++;
}
y ++;
x = 0;
}
}
function scanLine8Bit(ox, oy, ang, scale, r8, w8) {
var rx, ry, idxW, idxR, x = 0, y = 0;
const xAx = Math.cos(ang) / scale;
const xAy = Math.sin(ang) / scale;
w8.fill(0); // clears the buffer
while (y < H) {
const xx = x - ox, yy = y - oy;
rx = xx * xAx - yy * xAy + ox; // Get image coords for row start
ry = xx * xAy + yy * xAx + oy;
idxW = (y * W + x) * 4;
while (x < W) {
if (rx >= 0 && rx < W && ry >= 0 && ry < H) {
idxR = ((ry | 0) * W + (rx | 0)) * 4;
w8[idxW++] = r8[idxR++]; // red
w8[idxW++] = r8[idxR++]; // green
w8[idxW++] = r8[idxR++]; // blue
w8[idxW++] = r8[idxR++]; // alpha
} else {
idxW += 4;
}
rx += xAx;
ry += xAy;
x++;
}
y ++;
x = 0;
}
}
angSlider.addEventListener("input", rotate);
scaleSlider.addEventListener("input", rotate);
rotate();
function createTestImage() {
ctx.fillStyle = "#F00";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#FF0";
ctx.fillRect(20, 20, W - 40, H - 40);
ctx.fillStyle = "#00F";
ctx.fillRect(40, 40, W - 80, H - 80);
ctx.font = "120px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.lineWidth = 8;
ctx.lineJoin = "round";
ctx.fillStyle = "#000";
ctx.strokeStyle = "#FFF";
ctx.strokeText("Scanline!", W / 2, H / 2);
ctx.fillText("Scanline!", W / 2, H / 2);
}
.inputCont {
font-family: arial;
font-weight: 700;
position: absolute;
top: 10px;
left: 20px;
}
canvas {
position: absolute;
top: 0px;
left: 0px;
border: 1px solid black;
}
label { background: #FFF9 }
#scaleSlider { width: 400px }
#angSlider { width: 400px }
input[type=range]::-webkit-slider-runnable-track {
background: #8C8;
height: 8.4px;
cursor: pointer;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
border-radius: 4px;
border: 0.2px solid #010101;
padding-top: 0px;
}
input[type=range]::-webkit-slider-thumb {
margin-top: -5px;
}
<canvas id="canvas" width="512" height="320"></canvas>
<div class="inputCont">
<label for="angSlider">Angle</label>
<input id="angSlider" type="range" min="0" max="360" value="5" /><br>
<label for="scaleSlider">Scale</label>
<input id="scaleSlider" type="range" min="10" max="500" value="100" />
</div>
Note that in this example it is expected that the image and render result resolutions are the same size. This can easily be changed.
Note It uses a simple "nearest pixel" to get image pixels, so there will be artifacts due to aliasing. However it is very easy to add high quality anti aliasing (via sub pixel sampling) at the cost of performance.
Update
I have updated the example code re the comment "Would anything major need to change if I have to do this with arrays of r,g,b,a values?"
I have added comments instructing what to change and a second function scanLine8Bit that will render the content using arrays of bytes (8Bit) for the color channels RGBA. 4 bytes for each pixel.
I want the triangle to aim towards the center at all time while it is spinning as it is right now
var element = document.getElementById('background');
var ctx = element.getContext("2d");
var camera = {};
camera.x = 0;
camera.y = 0;
var scale = 1.0;
var obj = [];
var t = {};
t.angle = Math.random() * Math.PI * 2; //start angle
t.radius = 200;
t.x = Math.cos(t.angle) * t.radius; // start position x
t.y = Math.sin(t.angle) * t.radius; //start position y
t.duration = 10000; //10 seconds per rotation
t.rotSpeed = 2 * Math.PI / t.duration; // Rotational speed (in radian/ms)
t.start = Date.now();
obj.push(t);
function update() {
for (var i = 0; i < obj.length; i++) {
var delta = Date.now() - obj[i].start;
obj.start = Date.now();
var angle = obj[i].rotSpeed * delta;
// The angle is now already in radian, no longer need to convert from degree to radian.
obj[i].x = obj[i].radius * Math.cos(angle);
obj[i].y = obj[i].radius * Math.sin(angle);
}
}
function draw() {
update();
ctx.clearRect(0, 0, element.width, element.height);
ctx.save();
ctx.translate(0 - (camera.x - element.width / 2), 0 - (camera.y - element.height / 2));
ctx.scale(scale, scale);
ctx.fillStyle = 'blue';
for (var i = 0; i < obj.length; i++) {
/*Style circle*/
ctx.beginPath();
ctx.arc(0, 0, obj[i].radius, 0, Math.PI * 2);
ctx.lineWidth = 60;
ctx.strokeStyle = "black";
ctx.stroke();
//Dot style
ctx.beginPath();
ctx.arc(obj[i].x,obj[i].y,10,0,Math.PI*2);
ctx.moveTo(0 + obj[i].x, 0 + obj[i].y);
ctx.lineTo(75 + obj[i].x, 25 + obj[i].y);
ctx.lineTo(75 + obj[i].x, -25 + + obj[i].y);
ctx.lineWidth = 1.5;
ctx.strokeStyle = "red";
ctx.stroke();
ctx.fillStyle = "red";
ctx.fill();
}
ctx.restore();
requestAnimationFrame(draw);
}
draw();
<canvas id="background" width="500" height="500"></canvas>
Here is a quick fiddle that has all the code already in it just for convenience :)
https://jsfiddle.net/4xwmo5tj/5/
I have already made it spin (the dot is there for now but will be removed later so that can be ignored) the only thing that I still need to do is aim it towards the center at all time. I think I have to use css transform translate for it. but I don't know how to
Use CSS animations.
.outer-circle {
height: 200px;
width: 200px;
background-color: black;
padding: 25px;
position: relative;
border-radius: 50%;
-webkit-animation:spin 4s linear infinite;
-moz-animation:spin 4s linear infinite;
animation:spin 4s linear infinite;
}
.inner-cricle {
width: 150px;
height: 150px;
background: white;
margin: auto;
margin-top: 25px;
border-radius: 50%;
}
.triangle {
width: 0;
height: 0;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
border-top: 20px solid #f00;
position: absolute;
left: 40%;
top: 6%;
}
#-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
#-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
#keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
<div class="outer-circle">
<div class="triangle"></div><div class="inner-cricle"></div>
</div>
The triangle is not spinning at all. There is just a red dot circle in circumference and triangle inside black circle which is still. So what do you want to do?
The block of the code responsible for the triangle is under // Dot Style. The lines ctx.lineTo plot two dots at (x,y) coordinates. I fiddle around with the two coordinates until the triangle was facing down. Below is the end result:
var element = document.getElementById('background');
var ctx = element.getContext("2d");
var camera = {};
camera.x = 0;
camera.y = 0;
var scale = 1.0;
var obj = [];
var t = {};
t.angle = Math.random() * Math.PI * 2; //start angle
t.radius = 200;
t.x = Math.cos(t.angle) * t.radius; // start position x
t.y = Math.sin(t.angle) * t.radius; //start position y
t.duration = 10000; //10 seconds per rotation
t.rotSpeed = 2 * Math.PI / t.duration; // Rotational speed (in radian/ms)
t.start = Date.now();
obj.push(t);
function update() {
for (var i = 0; i < obj.length; i++) {
var delta = Date.now() - obj[i].start;
obj.start = Date.now();
var angle = obj[i].rotSpeed * delta;
// The angle is now already in radian, no longer need to convert from degree to radian.
obj[i].x = obj[i].radius * Math.cos(angle);
obj[i].y = obj[i].radius * Math.sin(angle);
}
}
function draw() {
update();
ctx.clearRect(0, 0, element.width, element.height);
ctx.save();
ctx.translate(0 - (camera.x - element.width / 2), 0 - (camera.y - element.height / 2));
ctx.scale(scale, scale);
ctx.fillStyle = 'blue';
for (var i = 0; i < obj.length; i++) {
/*Style circle*/
ctx.beginPath();
ctx.arc(0, 0, obj[i].radius, 0, Math.PI * 2);
ctx.lineWidth = 60;
ctx.strokeStyle = "white";
ctx.stroke();
//Dot style
ctx.beginPath();
ctx.arc(obj[i].x, obj[i].y, 10, 0, Math.PI * 2);
ctx.moveTo(0 + obj[i].x, 0 + obj[i].y);
ctx.lineTo(25 + obj[i].x, -75 + obj[i].y);
ctx.lineTo(-25 + obj[i].x, -75 + obj[i].y);
ctx.lineWidth = 1.5;
ctx.strokeStyle = "red";
ctx.stroke();
ctx.fillStyle = "red";
ctx.fill();
}
ctx.restore();
requestAnimationFrame(draw);
}
draw();
#background {
background: #000000;
border: 1px solid black;
}
<canvas id="background" width="500" height="500"></canvas>
Sorry, I can't provide any more specific details. The Cavas API and drawing are not my turfs.
I have a canvas animation which you can see here.
I've noticed that once you have watched the animation after a while (approximately 25 seconds) the animation starts to jump around. I'm struggling to figure out how to make it so that it is one constant fluid motion?
Code below:
<canvas id="canvas"></canvas>
var canvas = document.querySelector("#canvas");
var ctx = canvas.getContext("2d");
canvas.width = parseInt(getComputedStyle(canvas).width);
canvas.height = parseInt(getComputedStyle(canvas).height);
var P = 4;
var A = 4;
function draw(shift) {
var w = canvas.width;
var h = canvas.height;
shift = shift >= 500*Math.PI ? shift - 100*Math.PI : shift;
ctx.clearRect(0, 0, w, h);
var grd = ctx.createLinearGradient(0, 0, w, h);
grd.addColorStop(0, "#4a8bf5");
grd.addColorStop(1, "#f16b55");
ctx.strokeStyle = grd;
ctx.lineCap = "round";
for (var i = 0; i < w; ) {
var _A = Math.abs(A*Math.cos(2*i));
ctx.beginPath();
var pos = Math.exp(-_A * i / w) * Math.sin(P * Math.PI * (i + shift) / w);
pos *= h / 2;
var lw = Math.exp(-_A * i / w) * Math.sin(3 * Math.PI * (i - shift) / w) * 2;
ctx.lineWidth = (lw)+1;
ctx.lineTo(i, h / 2 - pos);
ctx.closePath();
ctx.stroke();
i += 1;
}
window.requestAnimationFrame(function(){
draw(shift + 1);
});
}
draw(0);
The solution is to make sure that the change in shift results in the sin() argument changing by a multiple of 2π.
Given
Math.sin((i + shift) / (w / P))
this can be done using something like
if (shift > 500) shift -= 2 * Math.PI * (w / P);
There will still be a jump in the 2nd sin() argument here, the line width. To avoid this, shift has to be reduced by a number that causes both arguments to change by multiples of 2π, the LCM if you will.
var canvas = document.querySelector("#canvas");
var ctx = canvas.getContext("2d");
canvas.width = parseInt(getComputedStyle(canvas).width);
canvas.height = parseInt(getComputedStyle(canvas).height);
var P = 10;
var A = 4;
var shift = 0;
function draw() {
var w = canvas.width;
var h = canvas.height;
shift += 1;
if (shift > 500) shift -= 2 * Math.PI * (w / P);
shift_el.innerHTML = shift;
ctx.clearRect(0, 0, w, h);
var grd = ctx.createLinearGradient(0, 0, w, h);
grd.addColorStop(0, "#4a8bf5");
grd.addColorStop(1, "#f16b55");
ctx.strokeStyle = grd;
ctx.lineCap = "round";
for (var i = 0; i < w;) {
var _A = Math.abs(A * Math.cos(2 * i));
ctx.beginPath();
var pos = Math.exp(-_A * i / w) * Math.sin((i + shift) / (w / P));
pos *= h / 2;
var lw = Math.exp(-_A * i / w) * Math.sin(3 * Math.PI * (i - shift) / w) * 2;
ctx.lineWidth = (lw) + 1;
ctx.lineTo(i, h / 2 - pos);
ctx.closePath();
ctx.stroke();
i += 1;
}
window.requestAnimationFrame(draw);
}
draw();
body {
background: #ffffff;
padding: 0;
margin: 0;
}
canvas {
height: 200px;
width: 100%;
}
#shift_el {
position: absolute;
top: 0
}
<canvas id="canvas"></canvas>
<div id="shift_el"></div>