I was working on canvas and came across the Idea of changing dimensions of the cube. So, by using HTML5 Canvas I made up this cube which has two squares joined by the lines to make it look like a cube.
What I want is when I select a cube type from select the cube should automatically change itself depending on the length and width of the selected option. The height remains constant. Like if the I select the cube of 5x5 which is by default a cube but when the I select the option of 5x10 the width(front) should not be changed but the length(side) of the cube should expand, and vice versa if I select 10x5 my max option is 25x15. As you can see the canvas I created below is in pixels, first I need to convert these pixels into centimeters(cm) then centimeters to cubic meters.
The whole cube should be aligned in the fixed canvas area specified.
Here is fiddle
var canvas = document.querySelector('canvas');
canvas.width = 500;
canvas.height = 300;
var contxt = canvas.getContext('2d');
//squares
/*
contxt.fillRect(x, y, widht, height);
*/
contxt.strokeStyle = 'grey';
var fillRect = false;
contxt.fillStyle = 'rgba(0, 0, 0, 0.2)';
contxt.rect(80, 80, 100, 100);
contxt.rect(120, 40, 100, 100);
if (fillRect) {
contxt.fill();
}
contxt.stroke();
/*Lines
contxt.beginPath();
contxt.moveTo(x, y);
contxt.lineTo(300, 100);
*/
contxt.beginPath();
contxt.moveTo(80, 80);
contxt.lineTo(120, 40);
contxt.moveTo(180, 80);
contxt.lineTo(220, 40);
contxt.moveTo(80, 180);
contxt.lineTo(120, 140);
contxt.moveTo(180, 180);
contxt.lineTo(220, 140);
contxt.stroke();
canvas {
border: 1px solid #000;
}
select {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<select>
<option>5x5</option>
<option>5x10</option>
<option>10x5</option>
</select>
<canvas></canvas>
Drawing the cube:
To generate a dynamic cube you would have to listen to an onChange event on the <select> element. Every time the selected option changes you would want to redraw your cube.
To redraw the cube you need to create a renderCube function which should take the new dimensions of the cube and as specified an offset for positioning. In this function you have to clear the previously drawn cube and redraw the new one with the given dimensions and offset.
Adding a transition effect:
As you can not apply css transitions to canvas elements you have to implement the transition yourself. You would have to create an animation function which would calculate the dimensions of the cube in the transition phase and rerender it to the screen on each frame.
An implementation of the resizable cube with a transition effect would be:
(if you prefer here is a fiddle too)
(if you do not need the transition effect check the fiddle before it has been implemented)
var canvas = document.querySelector('canvas');
canvas.width = 320;
canvas.height = 150;
var contxt = canvas.getContext('2d');
var currentHeight = 0, currentWidth = 0, currentDepth = 0, animationId = 0;
function renderCube(height, width, depth, offsetX, offsetY) {
currentHeight = height;
currentWidth = width;
currentDepth = depth;
// Clear possible existing cube
contxt.clearRect(0, 0, canvas.width, canvas.height);
contxt.beginPath();
// Calculate depth, width and height based on given input
depth = (depth * 10 * 0.8) / 2;
width = width * 10;
height = height * 10;
// Draw 2 squares to the canvas
contxt.strokeStyle = 'grey';
var fillRect = false;
contxt.fillStyle = 'rgba(0, 0, 0, 0.2)';
contxt.rect(offsetX, offsetY, width, height);
contxt.rect(offsetX + depth, offsetY - depth, width, height);
if (fillRect) {
contxt.fill();
}
contxt.stroke();
// An array which specifies where to draw the depth lines between the 2 rects
// The offset will be applied while drawing the lines
var depthLineCoordinates = [
// posX, posY, posX2, posY2
[0, 0, depth, -depth],
[width, 0, width + depth, -depth],
[0, height, depth, height - depth],
[width, height, width + depth, height - depth]
];
// Draw the depth lines to the canvas
depthLineCoordinates.forEach(function(element) {
contxt.moveTo(offsetX + element[0], offsetY + element[1]);
contxt.lineTo(offsetX + element[2], offsetY + element[3]);
});
contxt.stroke();
}
// As requested by OP an example of a transition to the cube
// The transitionDuration may be a double which specifies the transition duration in seconds
function renderCubeWithTransistion(height, width, depth, offsetX, offsetY, transitionDuration) {
var fps = 60;
var then = Date.now();
var startTime = then;
var finished = false;
var heightDifference = (height - currentHeight);
var widthDifference = (width - currentWidth);
var depthDifference = (depth - currentDepth);
// Get an "id" for the current animation to prevent multiple animations from running at the same time.
// Only the last recently started animation will be executed.
// If a new one should be run, the last one will get aborted.
var transitionStartMillis = (new Date()).getMilliseconds();
animationId = transitionStartMillis;
function animate() {
// Do not continue rendering the current animation if a new one has been started
if (transitionStartMillis != animationId) return;
// request another frame if animation has not been finished
if (!finished) requestAnimationFrame(animate);
// Control FPS
now = Date.now();
elapsed = now - then;
if (elapsed > (1000 / fps)) {
then = now - (elapsed % (1000 / fps));
// Calculate a linear transition effect
if (parseInt(currentHeight, 0) != parseInt(height, 0)) currentHeight += heightDifference / (transitionDuration * fps);
if (parseInt(currentWidth, 0) != parseInt(width, 0)) currentWidth += widthDifference / (transitionDuration * fps);
if (parseInt(currentDepth, 0) != parseInt(depth, 0)) currentDepth += depthDifference / (transitionDuration * fps);
// Render the cube
renderCube(currentHeight, currentWidth, currentDepth, offsetX, offsetY);
// Check if the current dimensions of the cube are equal to the specified dimensions of the cube
// If they are the same, finish the transition
if (parseInt(currentHeight, 0) === parseInt(height, 0) && parseInt(currentWidth, 0) === parseInt(width, 0) && parseInt(currentDepth, 0) === parseInt(depth, 0)) {
finished = true;
}
}
}
// Start the animation process
animate();
return true;
}
// Draw the cube initially with 5x5
renderCube(5, 5, 5, 80, 70);
// Add the onChange event listener to the select element
var cubeSizeSelector = document.getElementById('cubeSizeSelector');
cubeSizeSelector.onchange = function(e) {
var cubeSize = e.target.value.split('x');
renderCubeWithTransistion(5, parseInt(cubeSize[0], 0), parseInt(cubeSize[1], 0), 80, 70, 0.3);
}
canvas {
border: 1px solid #000;
}
select {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"> </script>
<select id="cubeSizeSelector">
<option>5x5</option>
<option>5x10</option>
<option>10x5</option>
</select>
<canvas></canvas>
Drawing an extruded outline. Axonometric
Ideally you would create a generic axonometric renderer that given a floor plan renders the object to the canvas as needed.
You can then link the plan to a selection box and update the view when the selection has changed.
Best as a code example
The example below uses the object renderIsoPlan to render the shape.
Shapes are set via a plan. eg a box has a floor plan [[-1,-1],[1,-1],[1,1],[-1,1]] representing the 4 bottom corners.
The renderIsoPlan has the following properties
canvas The canvas that the shape is rendered to. Will not draw until this is set. renderIsoPlan will create a 2D context which will be the same if you have one already
height How far up the outline is projected.
style Canvas context style object eg {stokeStyle : "red", lineWidth : 2} draws 2 pixel with red lines.
plan Set of points for the floor. Points are moved to center automatically. eg [[0,-1],[1,1],[-1,1]] draws a triangle
scale Scale say no more
rotate Amount to rotate. If not 0 then projection is dimetric else it is trimetric.
centerY in unit size of canvas. ie 0.5 is center
centerX same as centerY
Call renderIsoPlan.refresh to draw
Note that you can not rotate the projection in the question as it visually appears to warp (change shape) thus if rotate is not 0 then a different projection is used.
Note the object is automatically centered around 0,0 use centerX, centerY to center in the view
setTimeout(start,0); // wait till Javascript parsed and executed
requestAnimationFrame(animate); // Animate checked at start so start anim
// named list of shapes
const boxes = {
box1By1 : {
plan : [[-1,-1],[1,-1],[1,1],[-1,1]],
scale : 35,
centerY : 0.75,
},
box1By2 : {
plan : [[-1,-2],[1,-2],[1,2],[-1,2]],
scale : 30,
centerY : 0.7,
},
box2By2 : {
plan : [[-2,-2],[2,-2],[2,2],[-2,2]],
scale : 25,
centerY : 0.7,
},
box2By1 : {
plan : [[-2,-1],[2,-1],[2,1],[-2,1]],
scale : 30,
centerY : 0.7,
},
box1By3 : {
plan : [[-1,-3],[1,-3],[1,3],[-1,3]],
scale : 22,
centerY : 0.67,
},
box1By4 :{
plan : [[-1,-4],[1,-4],[1,4],[-1,4]],
scale : 20,
centerY : 0.63,
},
lShape : {
plan : [[-2,-4],[0,-4],[0,2],[2,2],[2,4],[-2,4]],
scale : 20,
centerY : 0.65,
},
current : null,
}
// Sets the renderIsoPlan object to the current selection
function setShape(){
boxes.current = boxes[boxShape.value];
Object.assign(renderIsoPlan, boxes.current);
if (!animateCheckBox.checked) { renderIsoPlan.refresh() }
}
// When ready this is called
function start(){
renderIsoPlan.canvas = canvas;
renderIsoPlan.height = 2;
setShape();
renderIsoPlan.refresh();
}
// Add event listeners for checkbox and box selection
boxShape.addEventListener("change", setShape );
animateCheckBox.addEventListener("change",()=>{
if (animateCheckBox.checked) {
requestAnimationFrame(animate);
} else {
renderIsoPlan.rotate = 0;
setShape();
}
});
// Renders animated object
function animate(time){
if (animateCheckBox.checked) {
renderIsoPlan.rotate = time / 1000;
renderIsoPlan.refresh();
requestAnimationFrame(animate);
}
}
// Encasulate Axonometric render.
const renderIsoPlan = (() => {
var ctx,canvas,plan,cx,cy,w,h,scale,height, rotate;
height = 50;
scale = 10;
rotate = 0;
const style = {
strokeStyle : "#000",
lineWidth : 1,
lineJoin : "round",
lineCap : "round",
};
const depthScale = (2/3);
// Transforms then projects the point to 2D
function transProjPoint(p) {
const project = rotate !== 0 ? 0 : depthScale;
const xdx = Math.cos(rotate);
const xdy = Math.sin(rotate);
const y = p[0] * xdy + p[1] * xdx;
const x = p[0] * xdx - p[1] * xdy - y * project;
return [x,y * depthScale];
}
// draws the plan
function draw() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0,0,w,h);
ctx.setTransform(scale, 0, 0, scale, cx, cy);
var i = plan.length;
ctx.beginPath();
while(i--){ ctx.lineTo(...transProjPoint(plan[i])) }
ctx.closePath();
i = plan.length;
ctx.translate(0,-height);
ctx.moveTo(...transProjPoint(plan[--i]))
while(i--){ ctx.lineTo(...transProjPoint(plan[i])) }
ctx.closePath();
i = plan.length;
while(i--){
const [x,y] = transProjPoint(plan[i]);
ctx.moveTo(x,y);
ctx.lineTo(x,y + height);
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.stroke();
}
// centers the plan view on coordinate 0,0
function centerPlan(plan){
var x = 0, y = 0;
for(const point of plan){
x += point[0];
y += point[1];
}
x /= plan.length;
y /= plan.length;
for(const point of plan){
point[0] -= x;
point[1] -= y;
}
return plan;
}
// Sets the style of the rendering
function setStyle(){
for(const key of Object.keys(style)){
if(ctx[key] !== undefined){
ctx[key] = style[key];
}
}
}
// define the interface
const API = {
// setters allow the use of Object.apply
set canvas(c) {
canvas = c;
ctx = canvas.getContext("2d");
w = canvas.width; // set width and height
h = canvas.height;
cx = w / 2 | 0; // get center
cy = h / 2 | 0; // move center down because plan is extruded up
},
set height(hh) { height = hh },
set style(s) { Object.assign(style,s) },
set plan(points) { plan = centerPlan([...points]) },
set scale(s) { scale = s },
set rotate(r) { rotate = r },
set centerY(c) { cy = c * h },
set centerX(c) { cx = c * w },
// getters not used in the demo
get height() { return height },
get style() { return style },
get plan() { return plan },
get scale() { return scale },
get rotate() { return r },
get centerY() { return cy / h },
get centerX() { return cx / w },
// Call this to refresh the view
refresh(){
if(ctx && plan){
ctx.save();
if(style){ setStyle() }
draw();
ctx.restore();
}
}
}
// return the interface
return API;
})();
canvas { border : 2px solid black; }
<select id="boxShape">
<option value = "box1By1">1 by 1</option>
<option value = "box1By2">1 by 2</option>
<option value = "box2By2">2 by 2</option>
<option value = "box2By1">2 by 1</option>
<option value = "box1By3">1 by 3</option>
<option value = "box1By4">1 by 4</option>
<option value = "lShape">L shape</option>
</select>
<input type="checkBox" id="animateCheckBox" checked=true>Animate</input><br>
<canvas id="canvas"></canvas>
I have an array like this, the idea is to output images in different directions and flip them vertically and horizonatly on a canvas by scaling.
[{
"pos":{
"x":411,
"y":401.5
},
"scale":{
"x":1,
"y":1
}
},{
"pos":{
"x":411,
"y":271.59625
},
"scale":{
"x":-1,
"y":1
}
}]
The problem is that I'm scaling the canvas instead of the images, the canvas is multiple times bigger than the images i'm placing on it.
images.forEach((image) => {
// center borde köars innan loopen egentligen
let pos = center(image.pos)
cc.save()
cc.scale(image.scale.x, image.scale.y)
cc.drawImage(window.video, pos.x, pos.y)
cc.restore()
})
How do I scale the image, called window.video, instead of the entire canvas?
To render a scaled image on the canvas.
function drawImage(image,x,y,scaleX,scaleY){
ctx.setTransform(scaleX, 0, 0, scaleY, x, y); // set scale and translation
ctx.drawImage(image,0,0);
}
// when done drawing images you need to reset the transformation back to default
ctx.setTransform(1,0,0,1,0,0); // set default transform
If you want the image flipped but still drawn at the same top left position use
function drawImage(image, x, y, scaleX, scaleY){
ctx.setTransform(scaleX, 0, 0, scaleY, x, y); // set scale and translation
x = scaleX < 0 ? -image.width : 0; // move drawing position if flipped
y = scaleY < 0 ? -image.height : 0;
ctx.drawImage(image, x, y);
}
And to draw about the center of the image
function drawImage(image, x, y, scaleX, scaleY){ // x y define center
ctx.setTransform(scaleX, 0, 0, scaleY, x, y); // set scale and translation
ctx.drawImage(image, -image.width / 2, -image.height / 2);
}
EDIT: As noted below, this doesn't work with negative values...
drawImage can take 2 extra arguments for sizing the image, so the following should work:
images.forEach((image) => {
// center borde köars innan loopen
let pos = center(image.pos)
cc.save()
cc.drawImage(window.video, pos.x, pos.y, window.video.width * image.scale.x, window.video.height * image.scale.y)
cc.restore()
})
Documentation: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Using_images
I want to achive the following:
Draw a bg-image to the canvas (once or if needed repeatedly)
The image should not be visible at the beginning
While i "paint" shapes to the canvas the bg-image should get visible where the shapes were drawn
The parts of the image that will be revealed shall be "painted" (like with a brush) so i want to use strokes.
What i tried:
- Do not clear the canvas
- Paint rects to the canvas with globalCompositeOperation = 'destination-in'
This works, the rectangles reveal the image but i need strokes
If i use strokes they are ignored with 'destination-in' while i see them with normal globalCompositeOperation.
Is this intended that the strokes are ignored? Is there a workaround like somehow converting the stroke/shape to a bitmap? Or do i have have to use two canvas elements?
In OpenGL i would first draw the image with its rgb values and with a = 0 and then only "paint" the alpha in.
You can solve it by these steps:
Set the image as a pattern
Set the pattern as fillStyle or strokeStyle
When you now fill/stroke your shapes the image will be revealed. Just make sure the initial image fits the area you want to reveal.
Example showing the principle, you should be able to adopt this to your needs:
var ctx = canvas.getContext("2d"),
img = new Image,
radius = 40;
img.onload = setup;
img.src = "http://i.imgur.com/bnAEEXq.jpg";
function setup() {
// set image as pattern for fillStyle
ctx.fillStyle = ctx.createPattern(this, "no-repeat");
// for demo only, reveals image while mousing over canvas
canvas.onmousemove = function(e) {
var r = this.getBoundingClientRect(),
x = e.clientX - r.left,
y = e.clientY - r.top;
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.arc(x, y, radius, 0, 2*Math.PI);
ctx.fill();
};
}
<canvas id=canvas width=900 height=600></canvas>
Hope this helps!
Alternative solution:
Put the image as a normal image on your website
add a canvas and use CSS positioning to place it right above the image
Fill the canvas with the color you use as the page background
have your paint tools erase the canvas when you draw. By the way, you can set context.globalCompositionOperation = 'destination-out' to turn all drawing operations into an eraser.
Here is an example. As you can see, the alpha properties of your tools are respected.
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
//prepare canvas
ctx.fillStyle = '#ffffff'
ctx.fillRect(0, 0, 120, 120);
//prepare a 30% opacity eraser
ctx.globalCompositeOperation = 'destination-out';
ctx.lineWidth = 5;
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
// make random strokes around cursor while mouse moves
canvas.onmousemove = function(e) {
var rect = this.getBoundingClientRect();
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
ctx.beginPath();
ctx.moveTo(x + Math.random() * 33 - 16, y + Math.random() * 33 - 16);
ctx.lineTo(x + Math.random() * 33 - 16, y + Math.random() * 33 - 16);
ctx.stroke();
}
<span>Move your mouse:</span>
<div>
<img src='https://upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/120px-HTML5_logo_and_wordmark.svg.png' style='position:absolute'>
<canvas id='canvas' width=120 height=120 style='position:absolute'></canvas>
</div>
I've created a basic HTML5 image slider where images move from top to bottom in a canvas.
I want all the images rotated at angle of 5 degrees. When I tried it out there seems to be some
distortion to the canvas and the image is not properly rotated.
I've tried the method for rotation mentioned in the below post
How do I rotate a single object on an html 5 canvas?
Fiddle - http://jsfiddle.net/DS2Sb/
Code
this.createImage = function (image, width, height) {
var fbWallImageCanvas = document.createElement('canvas');
var fbWallImageCanvasContext = fbWallImageCanvas.getContext('2d');
fbWallImageCanvas.width = width;
fbWallImageCanvas.height = height;
fbWallImageCanvasContext.save();
fbWallImageCanvasContext.globalAlpha = 0.7;
this.rotateImage(image, 0, 0, width, height, 5, fbWallImageCanvasContext);
fbWallImageCanvasContext.drawImage(image, width, height);
fbWallImageCanvasContext.restore();
return fbWallImageCanvas;
};
this.rotateImage = function (image, x, y, width, height, angle, context) {
var radian = angle * Math.PI / 180;
context.translate(x + width / 2, y + height / 2);
context.rotate(radian);
context.drawImage(image, width / 2 * (-1), height / 2 * (-1), width, height);
context.rotate(radian * (-1));
context.translate((x + width / 2) * (-1), (y + height / 2) * (-1));
};
The distortion you see is due to the fact that a rotated image will only fit in a larger canvas. So what we see is a rectangle view on a rotated image.
The computations are not that easy to get things done properly, but instead of pre-computing the rotated image, you might rotate them just when you draw them, which lets you also change the angle whenever you want (and opacity also btw).
So i simplified createImage, so that it just stores the image in a canvas (drawing a canvas is faster than drawing an image) :
this.createImage = function(image , width, height) {
var fbWallImageCanvas = document.createElement('canvas');
fbWallImageCanvas.width = width;
fbWallImageCanvas.height = height;
var fbWallImageCanvasContext = fbWallImageCanvas.getContext('2d');
fbWallImageCanvasContext.drawImage(image,0,0);
return fbWallImageCanvas;
};
And i changed drawItem so it draws the image rotated :
this.drawItem = function(ct) {
var angle = 5;
var radian = angle * Math.PI/180;
ct.save();
ct.translate(this.x + this.width/2 , this.y + this.height/2);
ct.rotate(radian);
ct.globalAlpha = 0.7;
ct.drawImage(fbc, - this.width/2, -this.height/2 , this.width, this.height);
ct.restore();
this.animate();
};
You'll probably want to refactor this, but you see the idea.
fiddle is here :
http://jsfiddle.net/DS2Sb/1/
Here is a link to a small html5 tutorial I created a while ago:
https://bitbucket.org/Garlov/html5-sidescroller-game-source
And here is the rotate code:
// save old coordinate system
ctx.save();
// move to the middle of where we want to draw our image
ctx.translate(canvas.width/2, canvas.height-64);
// rotate around that point
ctx.rotate(0.02 * (playerPosition.x));
//draw playerImage
ctx.drawImage(playerImage, -playerImage.width/2, -playerImage.height/2);
//and restore coordniate system to default
ctx.restore();