Javascript Canvas - CSS-rotating canvas elements leaves empty spaces between -- circular menu - javascript

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>

Related

Adding Background on Canvas

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>

js canvas object movement animation using pythagorean theorem

I am trying to make an object move to click position, like in rpgs like diablo or modern moba games, i needed to find a way to animate to click coordinates and i have found a method that uses pythagorean theorem, i understood and custimized the code to a certain degree, but there is a bug where the ball keeps bouncing in the very end of animation. I know this happens because of the "moves" variable and the loop, but can't understand exactly why.
here's the function that draws the object
function drawPlayer() {
//
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(player.x, player.y, 12, 0, Math.PI*2);
ctx.fillStyle = 'white';
ctx.fill();
ctx.closePath();
//Calculate variables
let dx = player.newx - player.x;
let dy = player.newy - player.y;
let distance = Math.sqrt(dx*dx + dy*dy);
let moves = distance/player.speed;
let xunits = (player.newx - player.x)/moves;
let yunits = (player.newy - player.y)/moves;
if (moves > 0 ) {
moves--;
player.x += xunits;
player.y += yunits;
console.log(moves);
}
}
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.addEventListener('click', e => {
setClickCoords(e);
drawPlayer();
})
canvas.width = 300;
canvas.height = 300;
const player = {
x: 0,
y: 0,
newx: 0,
newy: 0,
speed: 1,
radius: 15,
}
function drawPlayer() {
//Draw
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(player.x, player.y, 12, 0, Math.PI * 2);
ctx.fillStyle = 'white';
ctx.fill();
ctx.closePath();
//Calculate variables
let dx = player.newx - player.x;
let dy = player.newy - player.y;
let distance = Math.sqrt(dx * dx + dy * dy);
let moves = distance / player.speed;
let xunits = (player.newx - player.x) / moves;
let yunits = (player.newy - player.y) / moves;
console.log(moves);
if (moves > 0) {
moves--;
player.x += xunits;
player.y += yunits;
console.log(moves);
}
}
function setClickCoords(e) {
player.newx = e.offsetX;
player.newy = e.offsetY;
document.getElementById('oldcoords').innerHTML = "old Coords " + player.x + " " + player.y;
document.getElementById('newcoords').innerHTML = "new Coords " + player.newx + " " + player.newy;
}
function gameLoop() {
window.setTimeout(gameLoop, 24);
drawPlayer()
}
gameLoop();
body {
background: black;
display: flex;
flex-direction: column;
}
#canvas1 {
border: 3px solid white;
top: 50%;
left: 50%;
position: absolute;
width: 300px;
height: 300px;
transform: translate(-50%, -50%);
}
#oldcoords,
#newcoords {
color: white;
font-size: 18px;
}
<span id="oldcoords"></span>
<span id="newcoords"></span>
<canvas id="canvas1"> </canvas>
Check if moves is between 0 and 1. If it's between, move the ball to the desired position. Currently, the ball goes too far in the direction and has to return in the next animation.
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.addEventListener('click', e => {
setClickCoords(e);
drawPlayer();
})
canvas.width = 300;
canvas.height = 300;
const player = {
x: 0,
y: 0,
newx: 0,
newy: 0,
speed: 1,
radius: 15,
}
function drawPlayer() {
//Draw
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(player.x, player.y, 12, 0, Math.PI * 2);
ctx.fillStyle = 'white';
ctx.fill();
ctx.closePath();
//Calculate variables
let dx = player.newx - player.x;
let dy = player.newy - player.y;
let distance = Math.sqrt(dx * dx + dy * dy);
let moves = distance / player.speed;
console.log(moves);
if (moves > 1) {
let xunits = (player.newx - player.x) / moves;
let yunits = (player.newy - player.y) / moves;
moves--;
player.x += xunits;
player.y += yunits;
console.log(moves);
} else if (moves > 0) {
moves = 0;
player.x = player.newx;
player.y = player.newy;
console.log(moves);
}
}
function setClickCoords(e) {
player.newx = e.offsetX;
player.newy = e.offsetY;
document.getElementById('oldcoords').innerHTML = "old Coords " + player.x + " " + player.y;
document.getElementById('newcoords').innerHTML = "new Coords " + player.newx + " " + player.newy;
}
function gameLoop() {
window.setTimeout(gameLoop, 24);
drawPlayer()
}
gameLoop();
body {
background: black;
display: flex;
flex-direction: column;
}
#canvas1 {
border: 3px solid white;
top: 50%;
left: 50%;
position: absolute;
width: 300px;
height: 300px;
transform: translate(-50%, -50%);
}
#oldcoords,
#newcoords {
color: white;
font-size: 18px;
}
<span id="oldcoords"></span>
<span id="newcoords"></span>
<canvas id="canvas1"> </canvas>

Rotating an Image without CanvasRenderingContext2D.rotate()

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.

Aim triangle point towards center of circle while moving

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.

Why is there no way to rotate in canvas.getContext('2d').setTransform(a,b,c,d,e,f) and what is the best to do a rotate

I'm playing around with HTML5 canvas, and I am trying to implement a way of moving an image around on a canvas using translation, scaling, and rotation.
I have got translation and scaling working using setTransform:
canvas.getContext('2d').setTransform(a,b,c,d,e,f)
Which is handy as it discards previous transforms applied, then applies new ones, so there is no need to remember previous state when scaling etc.
On W3 schools is states that the 2nd and 3rd params are skewY and skewX, which I at first assumed to be rotate x and y. However after applying a transform passing some values to these params, it seems it doesn't rotate - it skews the canvas! (strange I know :-D).
Can anyone tell me why there is not rotate in set transform (I'm interested as it seems strange, and skew seems pretty useless to me), and also what is the best way to do a rotate around the center of a canvas along with using setTransform at the same time?
setTransform is based on a 2D Matrix (3x3). These kinds of matrices are used for 2D/3D projections and are typically handled by game engines these days, rather than the programmers who make games.
These things are a little bit linear-algebra and a little bit calculus (for the rotation).
You're not going to like this a whole lot, but here's what you're looking at doing:
function degs_to_rads (degs) { return degs / (180/Math.PI); }
function rads_to_degs (rads) { return rads * (180/Math.PI); }
Start with these helper functions, because while we think well in degrees, computers and math systems work out better in radians.
Then you want to start with calculating your rotation:
var rotation_degs = 45,
rotation_rads = degs_to_rads(rotation_degs),
angle_sine = Math.sin(rotation_rads),
angle_cosine = Math.cos(rotation_rads);
Then, based on the layout of the parameters:
ctx.setTransform(scaleX, skewY, skewX, scaleY, posX, posY);
in the following order, when rearranged into a transform matrix:
//| scaleX, skewX, posX |
//| skewY, scaleY, posY |
//| 0, 0, 1 |
...you'd want to submit the following values:
ctx.setTransform(angle_cosine, angle_sine, -angle_sine, angle_cosine, x, y);
// where x and y are now the "centre" of the rotation
This should get you rotation clockwise.
The marginal-benefit being that you should then be able to multiply everything by the scale that you initially wanted (don't multiply the posX and posY, though).
I have do some tests Based on the answer of #Norguard.
The following is the whole process of drawing a sprite on the canvas with translate, scale, rotate(at the center of rotation) and alpha(opacity):
var width = sprite.width;
var height = sprite.height;
var toX = sprite.transformOriginX * width;
var toY = sprite.transformOriginY * height;
// get the sin and cos value of rotate degree
var radian = sprite.rotate / 180 * Math.PI;
var sin = Math.sin(radian);
var cos = Math.cos(radian);
ctx.setTransform(
cos * sprite.scaleX,
sin * sprite.scaleX,
-sin * sprite.scaleY,
cos * sprite.scaleY,
sprite.x + toX,
sprite.y + toY
);
ctx.globalAlpha = sprite.alpha;
ctx.fillStyle = sprite.color;
ctx.fillRect(-toX, -toY, width, height);
And I made an interactive showcase you can play with:
// prepare the context
var myCanvas = document.getElementById('myCanvas');
var ctx = myCanvas.getContext('2d');
// say we have a sprite looks like this
var sprite = {
x: 50,
y: 50,
width: 50,
height: 100,
transformOriginX: 0.5, // the center of sprite width
transformOriginY: 0.5, // the center of sprite height
scaleX: 1.5,
scaleY: 1,
rotate: 45,
alpha: 0.5, // opacity
color: 'red'
};
function drawSprite() {
var width = sprite.width;
var height = sprite.height;
var scaleX = sprite.scaleX;
var scaleY = sprite.scaleY;
// get the transform-origin value
var toX = sprite.transformOriginX * width;
var toY = sprite.transformOriginY * height;
// get the sin and cos value of rotate degree
var radian = sprite.rotate / 180 * Math.PI;
var sin = Math.sin(radian);
var cos = Math.cos(radian);
ctx.setTransform(
cos * scaleX,
sin * scaleX,
-sin * scaleY,
cos * scaleY,
sprite.x + toX,
sprite.y + toY
);
ctx.globalAlpha = sprite.alpha;
ctx.fillStyle = sprite.color;
ctx.fillRect(-toX, -toY, width, height);
if (toShowInfo) {
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.moveTo(-toX + width / 2, -toY + height / 2);
ctx.lineTo(-toX + width / 2, -toY);
ctx.strokeStyle = 'lime';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-toX + width / 2, -toY + height / 2);
ctx.lineTo(-toX + width, -toY + height / 2);
ctx.strokeStyle = 'yellow';
ctx.stroke();
}
}
function draw() { // main launcher
// rest the ctx
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, myCanvas.width, myCanvas.height);
ctx.fillStyle = 'white';
ctx.font = '12px Arial';
ctx.textAlign = 'end';
ctx.textBaseline = 'hanging';
ctx.fillText('made by Rex Hsu', 395, 5);
// draw sprite
drawSprite();
// draw info
if (toShowInfo) { drawInfo(); };
}
function drawInfo() {
var x = sprite.x;
var y = sprite.y;
var width = sprite.width;
var height = sprite.height;
var toX = sprite.transformOriginX * width;
var toY = sprite.transformOriginY * height;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.arc(x + toX, y + toY, 3, 0, Math.PI * 2);
ctx.fillStyle = 'lime';
ctx.fill();
ctx.font = '12px Arial';
ctx.textAlign = 'start';
ctx.textBaseline = 'middle';
ctx.fillText('center of rotation', x + toX + 10, y + toY + 0);
ctx.beginPath();
ctx.rect(x, y, width, height);
ctx.strokeStyle = 'lime';
ctx.stroke();
}
function modifySprite() {
var name = this.id;
var value = this.value;
if (name !== 'color') {
value *= 1;
}
sprite[name] = value;
draw();
}
// init
var toShowInfo = true;
document.getElementById('checkbox').onchange = function() {
toShowInfo = !toShowInfo;
draw();
};
var propsDom = document.getElementById('props');
for (var i in sprite) {
var div = document.createElement('div');
var span = document.createElement('span');
var input = document.createElement('input');
span.textContent = i + ':';
input.id = i;
input.value = sprite[i];
input.setAttribute('type', 'text');
input.addEventListener('keyup', modifySprite.bind(input));
div.appendChild(span);
div.appendChild(input);
propsDom.appendChild(div);
}
draw();
body {
font-family: monospace;
}
canvas {
float: left;
background-color: black;
}
div {
float: left;
margin: 0 0 5px 5px;
}
div > div {
float: initial;
}
span {
font-size: 16px;
}
input[type="text"] {
margin: 0 0 5px 5px;
color: #999;
border-width: 0 0 1px 0;
}
<canvas id="myCanvas" width="400" height="400"></canvas>
<div id="props" style="float: left; width: calc(100% - 400px - 5px);">
<div style="float: initial;">
<input type="checkbox" id="checkbox" checked><span>Show origin-shape and the center of rotation</span>
</div>
</div>

Categories