How to draw a fraction of a bezier curve with canvas? - javascript

Let's say I have a bezier curve produced from the following code:
const ctx = document.querySelector('canvas').getContext('2d');
ctx.beginPath();
ctx.moveTo(50, 20);
ctx.quadraticCurveTo(230, 30, 50, 100);
ctx.stroke();
<canvas></canvas>
Is there a way to only draw, say, the last 90% of it?
For my application I want to "consume" the curve, and create an animation where a circle moves along the line path, eating the curve along the way.
The only thing I could think of was to instead of drawing the curve using the quadraticCurveTo function, to instead calculate a huge list of points manually through the following function:
t = 0.5; // given example value
x = (1 - t) * (1 - t) * p[0].x + 2 * (1 - t) * t * p[1].x + t * t * p[2].x;
y = (1 - t) * (1 - t) * p[0].y + 2 * (1 - t) * t * p[1].y + t * t * p[2].y;
And then just do moveTo and lineTo for each of the 300 or so points.
But that has three issues:
It is computationally expensive
How do you determine how many points to calculate?
Won't it still be jagged unless you calculate thousands of points?
Is there a better way?

You can use ctx.setLineDash([]) with ctx.lineDashOffset, that's the common way to simulate partial drawing of paths.
const ctx = document.querySelector('canvas').getContext('2d');
animate();
function animate(){
let i = 0;
drawCurve(i);
function drawCurve(start){
ctx.clearRect(0,0,300,150); // initial width and height of canvas
const line_len = 204; // to let the last part of curve stay
ctx.setLineDash([1000]); // bigger than curve length
ctx.lineDashOffset = -start; // changing parameter
ctx.beginPath();
ctx.moveTo(50, 20);
ctx.quadraticCurveTo(230, 30, 50, 100);
ctx.stroke();
const anim_id = requestAnimationFrame(() => drawCurve(++start));
if(start > line_len) cancelAnimationFrame(anim_id);
}
}
<canvas></canvas>
It can be tricky to get length of the path in canvas. So I prefer to calculate it using hidden SVG. The <path> of it has empty d attribute. So I can assign our path string to it and getTotalLength() of it. Now we have length of your path and we can use those data to define setLineDash array properly, so the path_len (where to stop).
Also we can get the current position of path start by using path.getPointAtLength() method.
animate();
function animate(){
const path = document.querySelector('svg > path');
const path_string = 'M 50 20 Q 230 30 50 100';
path.setAttribute('d', path_string);
const path_len = path.getTotalLength();
const ctx = document.querySelector('canvas').getContext('2d');
drawCurve(0);
function drawCurve(start){
ctx.clearRect(0,0,300,150); // initial width and height of canvas
ctx.save();
ctx.setLineDash([path_len + 1]); // bigger than curve length
ctx.lineDashOffset = -start; // changing parameter
ctx.stroke(new Path2D(path_string));
ctx.restore();
const cur_pos = path.getPointAtLength(start - 7); // current position - (radius + 2)
ctx.beginPath();
ctx.arc(cur_pos.x, cur_pos.y, 5, 0, 2*Math.PI, false); // radius = 5 (should be a constant)
ctx.fill();
const anim_id = requestAnimationFrame(() => drawCurve(++start));
if(start > path_len) cancelAnimationFrame(anim_id);
}
}
<svg style="display:none">
<path d=""></path>
</svg>
<canvas></canvas>

Related

HTML5 Canvas atan2 off by 90 degrees

I was trying to get the green triangle to rotate about its center and orient itself towards the mouse position. I was able to accomplish this, and you can view the full code and result here:
https://codepen.io/Carpetfizz/project/editor/DQbEVe
Consider the following lines of code:
r = Math.atan2(mouseY - centerY, mouseX - centerX)
ctx.rotate(r + Math.PI/2)
I arbitrarily added Math.PI/2 to my angle calculation because without it, the rotations seemed to be 90 degrees off (by inspection). I want a better understanding of the coordinate system which atan2 is being calculated with respect to so I can justify the reason for offsetting the angle by 90 degrees (and hopefully simplify the code).
EDIT:
To my understanding, Math.atan2 is measuring the angle illustrated in blue. Shouldn't rotating both triangles that blue angle orient it towards the mouse mouse pointer (orange dot) ? Well - obviously not since it's the same angle and they are two different orientations, but I cannot seem to prove this to myself.
This is because of how the Math.atan2 works.
From MDN:
This is the counterclockwise angle, measured in radians, between the positive X axis, and the point (x, y).
In above figure, the positive X axis is the horizontal segment going from the junction to the right-most position.
To make it clearer, here is an interactive version of this diagram, where x, y values are converted to [-1 ~ 1] values.
const ctx = canvas.getContext('2d'),
w = canvas.width,
h = canvas.height,
radius = 0.3;
ctx.textAlign = 'center';
canvas.onmousemove = canvas.onclick = e => {
// offset mouse values so they are relative to the center of our canvas
draw(as(e.offsetX), as(e.offsetY));
}
draw(0, 0);
function draw(x, y) {
clear();
drawCross();
drawLineToPoint(x, y);
drawPoint(x, y);
const angle = Math.atan2(y, x);
drawAngle(angle);
writeAngle(angle);
}
function clear() {
ctx.clearRect(0, 0, w, h);
}
function drawCross() {
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(s(0), s(-1));
ctx.lineTo(s(0), s(1));
ctx.moveTo(s(-1), s(0));
ctx.lineTo(s(0), s(0));
ctx.strokeStyle = ctx.fillStyle = '#2e404f';
ctx.stroke();
// positive X axis
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(s(0), s(0));
ctx.lineTo(s(1), s(0));
ctx.stroke();
ctx.lineWidth = 1;
ctx.font = '20px/1 sans-serif';
ctx.fillText('+X', s(1) - 20, s(0) - 10);
}
function drawPoint(x, y) {
ctx.beginPath();
ctx.arc(s(x), s(y), 10, 0, Math.PI * 2);
ctx.fillStyle = 'red';
ctx.fill();
ctx.font = '12px/1 sans-serif';
ctx.fillText(`x: ${x.toFixed(2)} y: ${y.toFixed(2)}`, s(x), s(y) - 15);
}
function drawLineToPoint(x, y) {
ctx.beginPath();
ctx.moveTo(s(0), s(0));
ctx.lineTo(s(x), s(y));
ctx.strokeStyle = 'red';
ctx.setLineDash([5, 5]);
ctx.stroke();
ctx.setLineDash([0]);
}
function drawAngle(angle) {
ctx.beginPath();
ctx.moveTo(s(radius), s(0));
ctx.arc(s(0), s(0), radius * w / 2,
0, // 'arc' method also starts from positive X axis (3 o'clock)
angle,
true // Math.atan2 returns the anti-clockwise angle
);
ctx.strokeStyle = ctx.fillStyle = 'blue';
ctx.stroke();
ctx.font = '20px/1 sans-serif';
ctx.fillText('∂: ' + angle.toFixed(2), s(0), s(0));
}
// below methods will add the w / 2 offset
// because canvas coords set 0, 0 at top-left corner
// converts from [-1 ~ 1] to px
function s(value) {
return value * w / 2 + (w / 2);
}
// converts from px to [-1 ~ 1]
function as(value) {
return (value - w / 2) / (w / 2);
}
<canvas id="canvas" width="500" height="500"></canvas>
So now, if we go back to your image, it currently points to the top (positive Y axis), while the angle you just measured is realtive to the x axis, so it doesn't point where you intended.
Now we know the problem, the solution is quite easy:
either apply the + Math.PI / 2 offset to your angle like you did,
either modify your original image so that it points to the positive X axis directly.
The coordinate system on canvas works with 0° pointing right. This means anything you want to point "up" must be initially drawn right.
All you need to do in this case is to change this drawing:
to
pointing "up" 0°
and you can strip the math back to what you'd expect it to be.
var ctx = c.getContext("2d"), img = new Image;
img.onload = go; img.src = "https://i.stack.imgur.com/Yj9DU.jpg";
function draw(pos) {
var cx = c.width>>1,
cy = c.height>>1,
angle = Math.atan2(pos.y - cy, pos.x - cx);
ctx.setTransform(1,0,0,1,cx, cy);
ctx.rotate(angle);
ctx.drawImage(img, -img.width>>1, -img.height>>1);
}
function go() {
ctx.globalCompositeOperation = "copy";
window.onmousemove = function(e) {draw({x: e.clientX, y: e.clientY})}
}
html, body {margin:0;background:#ccc}
#c {background:#fff}
<canvas id=c width=600 height=600></canvas>
When you do arctangents in math class, you're generally dealing with an y-axis that increases going upwards. In most computer graphics systems, however, including canvas graphics, y increases going downward. [erroneous statement deleted]
Edit: I have to admit what I wrote before was wrong for two reasons:
A change in the direction of the axis would be compensated for by adding π, not π/2.
The canvas context rotate function rotates clockwise for positive angles, and that alone should compensate for the flip of the y-axis.
I played around with a copy of your code in Plunker, and now I realize the 90° rotation simply compensates for the starting orientation of the graphic image you're drawing. If the arrowhead pointed right to start with, instead of straight up, you wouldn't need to add π/2.
I encountered the same problem and was able to achieve the desired result with a following axis 'trick':
// Default usage (works fine if your image / shape points to the RIGHT)
let angle = Math.atan2(delta_y, delta_x);
// 'Tricky' usage (works fine if your image / shape points to the LEFT)
let angle = Math.atan2(delta_y, -delta_x);
// 'Tricky' usage (works fine if your image / shape points to the BOTTOM)
let angle = Math.atan2(delta_x, delta_y);
// 'Tricky' usage (works fine if your image / shape points to the TOP)
let angle = Math.atan2(delta_x, -delta_y);

Advanced image skewing in JavaScript, fill polygon with image [duplicate]

I have an image which is a background containing a boxed area like this:
I know the exact positions of the corners of that shape, and I'd like to place another image within it. (So it appears to be inside the box).
I'm aware of the drawImage method for HTML5 canvas, but it seems to only support x, y, width, height parameters rather than exact coordinates. How might I draw an image onto a canvas at a specific set of coordinates, and ideally have the browser itself handle stretching the image.
Quadrilateral transform
One way to go about this is to use Quadrilateral transforms. They are different than 3D transforms and would allow you to draw to a canvas in case you want to export the result.
The example shown here is simplified and uses basic sub-divison and "cheats" on the rendering itself - that is, it draws in a small square instead of the shape of the sub-divided cell but because of the small size and the overlap we can get away with it in many non-extreme cases.
The proper way would be to split the shape into two triangles, then scan pixel wise in the destination bitmap, map the point from destination triangle to source triangle. If the position value was fractional you would use that to determine pixel interpolation (f.ex. bi-linear 2x2 or bi-cubic 4x4).
I do not intend to cover all this in this answer as it would quickly become out of scope for the SO format, but the method would probably be suitable in this case unless you need to animate it (it is not performant enough for that if you want high resolution).
Method
Lets start with an initial quadrilateral shape:
The first step is to interpolate the Y-positions on each bar C1-C4 and C2-C3. We're gonna need current position as well as next position. We'll use linear interpolation ("lerp") for this using a normalized value for t:
y1current = lerp( C1, C4, y / height)
y2current = lerp( C2, C3, y / height)
y1next = lerp(C1, C4, (y + step) / height)
y2next = lerp(C2, C3, (y + step) / height)
This gives us a new line between and along the outer vertical bars.
Next we need the X positions on that line, both current and next. This will give us four positions we will fill with current pixel, either as-is or interpolate it (not shown here):
p1 = lerp(y1current, y2current, x / width)
p2 = lerp(y1current, y2current, (x + step) / width)
p3 = lerp(y1next, y2next, (x + step) / width)
p4 = lerp(y1next, y2next, x / width)
x and y will be the position in the source image using integer values.
We can use this setup inside a loop that will iterate over each pixel in the source bitmap.
Demo
The demo can be found at the bottom of the answer. Move the circular handles around to transform and play with the step value to see its impact on performance and result.
The demo will have moire and other artifacts, but as mentioned earlier that would be a topic for another day.
Snapshot from demo:
Alternative methods
You can also use WebGL or Three.js to setup a 3D environment and render to canvas. Here is a link to the latter solution:
Three.js
and an example of how to use texture mapped surface:
Three.js texturing (instead of defining a cube, just define one place/face).
Using this approach will enable you to export the result to a canvas or an image as well, but for performance a GPU is required on the client.
If you don't need to export or manipulate the result I would suggest to use simple CSS 3D transform as shown in the other answers.
/* Quadrilateral Transform - (c) Ken Nilsen, CC3.0-Attr */
var img = new Image(); img.onload = go;
img.src = "https://i.imgur.com/EWoZkZm.jpg";
function go() {
var me = this,
stepEl = document.querySelector("input"),
stepTxt = document.querySelector("span"),
c = document.querySelector("canvas"),
ctx = c.getContext("2d"),
corners = [
{x: 100, y: 20}, // ul
{x: 520, y: 20}, // ur
{x: 520, y: 380}, // br
{x: 100, y: 380} // bl
],
radius = 10, cPoint, timer, // for mouse handling
step = 4; // resolution
update();
// render image to quad using current settings
function render() {
var p1, p2, p3, p4, y1c, y2c, y1n, y2n,
w = img.width - 1, // -1 to give room for the "next" points
h = img.height - 1;
ctx.clearRect(0, 0, c.width, c.height);
for(y = 0; y < h; y += step) {
for(x = 0; x < w; x += step) {
y1c = lerp(corners[0], corners[3], y / h);
y2c = lerp(corners[1], corners[2], y / h);
y1n = lerp(corners[0], corners[3], (y + step) / h);
y2n = lerp(corners[1], corners[2], (y + step) / h);
// corners of the new sub-divided cell p1 (ul) -> p2 (ur) -> p3 (br) -> p4 (bl)
p1 = lerp(y1c, y2c, x / w);
p2 = lerp(y1c, y2c, (x + step) / w);
p3 = lerp(y1n, y2n, (x + step) / w);
p4 = lerp(y1n, y2n, x / w);
ctx.drawImage(img, x, y, step, step, p1.x, p1.y, // get most coverage for w/h:
Math.ceil(Math.max(step, Math.abs(p2.x - p1.x), Math.abs(p4.x - p3.x))) + 1,
Math.ceil(Math.max(step, Math.abs(p1.y - p4.y), Math.abs(p2.y - p3.y))) + 1)
}
}
}
function lerp(p1, p2, t) {
return {
x: p1.x + (p2.x - p1.x) * t,
y: p1.y + (p2.y - p1.y) * t}
}
/* Stuff for demo: -----------------*/
function drawCorners() {
ctx.strokeStyle = "#09f";
ctx.lineWidth = 2;
ctx.beginPath();
// border
for(var i = 0, p; p = corners[i++];) ctx[i ? "lineTo" : "moveTo"](p.x, p.y);
ctx.closePath();
// circular handles
for(i = 0; p = corners[i++];) {
ctx.moveTo(p.x + radius, p.y);
ctx.arc(p.x, p.y, radius, 0, 6.28);
}
ctx.stroke()
}
function getXY(e) {
var r = c.getBoundingClientRect();
return {x: e.clientX - r.left, y: e.clientY - r.top}
}
function inCircle(p, pos) {
var dx = pos.x - p.x,
dy = pos.y - p.y;
return dx*dx + dy*dy <= radius * radius
}
// handle mouse
c.onmousedown = function(e) {
var pos = getXY(e);
for(var i = 0, p; p = corners[i++];) {if (inCircle(p, pos)) {cPoint = p; break}}
}
window.onmousemove = function(e) {
if (cPoint) {
var pos = getXY(e);
cPoint.x = pos.x; cPoint.y = pos.y;
cancelAnimationFrame(timer);
timer = requestAnimationFrame(update.bind(me))
}
}
window.onmouseup = function() {cPoint = null}
stepEl.oninput = function() {
stepTxt.innerHTML = (step = Math.pow(2, +this.value));
update();
}
function update() {render(); drawCorners()}
}
body {margin:20px;font:16px sans-serif}
canvas {border:1px solid #000;margin-top:10px}
<label>Step: <input type=range min=0 max=5 value=2></label><span>4</span><br>
<canvas width=620 height=400></canvas>
You can use CSS Transforms to make your image look like that box. For example:
img {
margin: 50px;
transform: perspective(500px) rotateY(20deg) rotateX(20deg);
}
<img src="https://via.placeholder.com/400x200">
Read more about CSS Transforms on MDN.
This solution relies on the browser performing the compositing. You put the image that you want warped in a separate element, overlaying the background using position: absolute.
Then use CSS transform property to apply any perspective transform to the overlay element.
To find the transform matrix you can use the answer from: How to match 3D perspective of real photo and object in CSS3 3D transforms

One canvas to interact, three to display

i'm creating a browser game which is meant to be played as a hologram.
The screen should be displaying something like this:
https://www.youtube.com/watch?v=Y60mfBvXCj8
Therefore i thought i have to create 4 canvas (no problem), but three of them should only display whats happening on the first.
I've tried to let it draw an Image of the canvas and let it display to the other canvas.
Any help would be appreciated!
The game is created with Box2D.
edit:
i want the space ship to be drawn in every canvas, but only controlled in one.
my code: http://s000.tinyupload.com/index.php?file_id=68837773176112789787
the problem is, that its only displaying on one canvas!
what i've put in the HTML:
<canvas id="canvas1" width="500" height="500"></canvas>
<canvas id="canvas2" width="500" height="500"></canvas>
<canvas id="canvas3" width="500" height="500"></canvas>
<canvas id="canvas4" width="500" height="500"></canvas>
what is meant to print it to the others:
JS
var sourceCtx, destinationCtx, imageData;
//get the context of each canvas
sourceCtx = canvas2.getContext('2d');
canvas2Ctx = canvas3.getContext('2d');
//copy the data
imageData = sourceCtx.getImageData(0, 0, canvas2.width - 1, canvas2.height - 1);
//apply the image data
canvas3Ctx.putImageData(imageData, 0, 0);
//done
Holographic pyramid display
How to render for a pyramid reflecting display.
To do this use a single display canvas in the HTML and a canvas stored in memory for rendering.
Mirrored render canvas
The rendering canvas is clipped to a triangle to prevent pixels overlapping and the transform is mirrored so that the final effect is correctly seen. Eg text is back to front.
The offscreen rendering canvas is then rendered to the display canvas, starting at the top and making a total of 4 copies each rotated 90deg.
The rendering canvas width will be the minimum of the display width or height and half that for the height in order to fit the display.
Needs fullscreen mode
For the FX to work you will need to enter fullscreen mode. I have not included how this is done but I am sure there is a QA on stackoverflow that will step you through the process.
Dead zone
At the center of the display is a area on which the pyramid will rest (I call it the dead zone) As many of these displays are homemade the size of the dead zone will vary. In the very first line of the demo below is a constant deadZoneSize that will set the dead zone size. It is currently set at 0.1 which is 10% of the view size. You may need to adjust this value to suit your particular reflecting display.
Example code
The code example is full of comments in the relevant parts. It will create and setup the display canvas and render canvas. Create the clip area and set up the mirrored rendering transform, so you can render as normal. A mainLoop function will call a function called renderContent with the first argument as being the context of the render canvas. Just render your content as normal (use size and hSize for the width and height of the visible render area (maybe I should have used a better name))
The demo includes an example rendering just for the fun of it, that is all at the bottom and has minimum comments as not really relevant to the question.
const deadZoneSize = 0.1; // As fraction of fitted box size
// for FX em and em4 are just custom unit size and 1/4 size
var em,em4;
// to fit all four views use the min width or height
var size = Math.min(innerWidth,innerHeight);
// half size
var hSize = size / 2 | 0;
// there is a small area where nothing should be displayed.
// This will depend on the pyrimide being used.
var deadZone = size * 0.1 | 0; // about 10% of view area
// Display canvas d for display
const dCanvas = document.createElement("canvas");
// Render canvas
const rCanvas = document.createElement("canvas");
// get rendering context for both
const dCtx = dCanvas.getContext("2d");
const rCtx = rCanvas.getContext("2d");
// Set the display canvas to fill the page
Object.assign(dCanvas.style,{
position : "absolute",
zIndex : 10, // place above
top : "0px",
left : "0px",
background : "black",
})
// add the display canvas to the DOM
document.body.appendChild(dCanvas);
//Size function resizes canvases when needed
function resize(){
startTime = undefined;
size = Math.min(innerWidth,innerHeight);
hSize = size / 2 | 0;
deadZone = size * deadZoneSize | 0; // about 10% of view area
dCanvas.width = innerWidth;
dCanvas.height = innerHeight;
rCanvas.width = size;
rCanvas.height = hSize; // half height
em = size * 0.1 | 0; // define our own unit size
em4 = Math.max(1,em * 0.25 | 0); // define quarter unit size min of 1
}
// To ensure pixels do not stray outside the view area and overlap use a clip on the render canvas
// ctx the context to appy the clip path to
function defineClip(ctx){
ctx.beginPath();
ctx.lineTo(0,0);
ctx.lineTo(size,0);
ctx.lineTo(hSize + deadZone, hSize - deadZone);
ctx.lineTo(hSize - deadZone, hSize - deadZone);
ctx.clip();
// The rendering is mirrored from the holo pyramid
// to avoid seeing text mirrored you need to mirror the
// rendering transform
ctx.setTransform(-1,0,0,1,size,0); // x axis from right to left, origin at top right
}
// Copying the rendered canvas to the display canvas
// ctx is the display canvas context
// image is the rendered canvas
function display(ctx,image) {
// for each face of the pyramid render a view
// Each image is just rotated 90 deg
// first clear the canvas
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
// top
// use the center of the display canvas as the origin
ctx.setTransform(1,0,0,1,ctx.canvas.width / 2 | 0, ctx.canvas.height / 2 | 0);
// draw the image
ctx.drawImage(image,-hSize,-hSize);
// Right
ctx.transform(0,1,-1,0,0,0); // rotate 90 deg. This is better than ctx.rotate as it can have slight
// problems due to floating point errors if not done correctly
ctx.drawImage(image,-hSize,-hSize);
// bottom
ctx.transform(0,1,-1,0,0,0);
ctx.drawImage(image,-hSize,-hSize);
// left
ctx.transform(0,1,-1,0,0,0);
ctx.drawImage(image,-hSize,-hSize);
// restore the default transform;
ctx.setTransform(1,0,0,1,0,0);
}
// the main render loop
var globalTime;
var startTime;
function mainLoop(time){
// check canvas size. If not matching page then resize
if(dCanvas.width !== innerWidth || dCanvas.height !== innerHeight) {
resize();
}
if(startTime === undefined){ startTime = time }
globalTime = time - startTime;
// clear the render canvas ready for next render
rCtx.setTransform(1,0,0,1,0,0); // reset transform
rCtx.globalAlpha = 1; // reset alpha
rCtx.clearRect(0,0,size,hSize);
// save the context state so that the clip can be removed
rCtx.save();
defineClip(rCtx); // set the clip
renderContent(rCtx); // call the rendering function
// restore the context state which removes the clip
rCtx.restore();
// rendering is ready for display so render the holo view
// on to the display canvas's context
display(dCtx, rCanvas);
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
//=====================================================================================================
// The following is just something interesting to display and is not directly related to the answer
//=====================================================================================================
// The main rendering function
// This is where you render your content. It can be anything from a game to just plain old text
// You can even use a video element and display a video.
// The rendering context is already set up to correctly mirror the content so just render everything as normal
const randG = (min, max , p = 2) => (max + min) / 2 + (Math.pow(Math.random(), p) * (max - min) * 0.5) * (Math.random() < 0.5 ? 1 : -1);
const bootUp = ["Power On",1,1000,"Sub system test",0.5, 3000, "Calibrating scanner",0.5, 6000, "Welcome",1,8000];
function noisyText(ctx){
var textTime = globalTime / 8000; // 8 second boot up
if(screenFlashDone){
if(globalTime > screenFlashes[0]) { // play screen flash seq
screenFlashes.shift();
screenFlash(ctx,true,screenFlashes.shift(),screenFlashes.shift());
}
}else{
screenFlash(ctx);
}
ctx.font = ((bootUp[1] * em) | 0) + "px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "center";
var tx = randG(-em4 * 4, em4 * 4, 64); // G for kind of a bit like gausian. Last num controls distrubution
var ty = randG(-em4 * 4, em4 * 4, 64);
var xx = size / 2 + tx;
var yy = em * 2 + ty;
ctx.fillStyle = `hsl(${randG(160,250,32)|0},100%,50%)`;
if(bootUp[2] < globalTime){
bootUp.shift();
bootUp.shift();
bootUp.shift();
}
ctx.fillText(bootUp[0], xx, yy);
ctx.save(); // need the normal non mirror transform for the noise FX
ctx.setTransform(1,0,0,1,0,0);
for(var y = -em/1.2|0; y < em/2; y += 1){
if((yy+y) % 3 === 0){
ctx.clearRect(0,yy+y,size,1); // give scan line look
}else{
if(Math.random() < 0.1){ // only on 10% of lines.
ctx.drawImage(ctx.canvas,0,yy + y, size, 2,randG(-em4 * 4,em4 * 4,32),yy + y, size, 2);
}
}
}
ctx.fillRect(0,((globalTime / 4000) * hSize)%hSize,size,2);
ctx.filter = `blur(${randG(em4/2,em4,2)|0}px)`;
ctx.drawImage(ctx.canvas,0,0);
ctx.restore();
}
const screenFlashes = [0,500,3,1000,200,2,4000,100,3,6000,100,1,7500,50,1,7800,50,1, 9000];
var screenFlashStart;
var screenFlashLen;
var screenFlashDone = true;
var screenFlashLayers = 1;
function screenFlash(ctx,start,length,layers){
if(start){
screenFlashStart = globalTime;
screenFlashLen = length;
screenFlashDone = false;
screenFlashLayers = layers;
}
var normTime = (globalTime - screenFlashStart) / screenFlashLen;
if(normTime >= 1){
screenFlashDone = true;
normTime = 1;
}
for(var i = 0; i < screenFlashLayers; i++){
var tx = randG(-em4 * 4, em4 * 4, 64); // G for kind of a bit like gausian. Last num controls distrubution
var ty = randG(-em4 * 4, em4 * 4, 64);
ctx.globalAlpha = (1-normTime) * Math.random();
ctx.fillStyle = `hsl(${randG(160,250,32)|0},100%,50%)`;
ctx.fillRect(tx,ty,size,hSize);
}
ctx.globalAlpha = 1;
}
function randomBlur(ctx) {
ctx.save(); // need the normal non mirror transform for the noise FX
ctx.filter = `blur(${randG(em4/2,em4,2)|0}px)`;
ctx.drawImage(ctx.canvas,0,0);
ctx.restore();
}
function ready(ctx) {
ctx.fillStyle = "#0F0";
ctx.font = em + "px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "center";
ctx.fillText("Holographic",hSize,em);
ctx.font = em/2 + "px monospace";
ctx.fillText("display ready.",hSize,em * 2);
// draw edges
ctx.strokeStyle = "#0F0";
ctx.lineWidth = em4;
ctx.beginPath();
ctx.lineTo(0,0);
ctx.lineTo(size,0);
ctx.lineTo(hSize + deadZone, hSize - deadZone);
ctx.lineTo(hSize - deadZone, hSize - deadZone);
ctx.closePath();
ctx.stroke();
}
function renderContent(ctx){
// all rendering is mirrored, but the transform takes care of that for you
// just render as normal. Remember you can only see the
// triangular area with the wide part at the top
// and narrow at the bottom.
// Anything below hSize - deadZone will also not appear
if(globalTime < 8000){
noisyText(ctx);
randomBlur(ctx);
}else{
ready(ctx);
}
randomBlur(ctx);
}
A quick side note. I feel your question meets the SO requirements and is not off topic, nor are you asking for someone to write the code. You have shown that you have put some effort into research. This question will be of interest to others. I hope this answer helps, good luck in your project and welcome to SO.

Creating a Gradient Path Fill JavaScript

I've been recently adding shadows to a project. I've ended up with something that I like, but the shadows are a solid transparent color throughout. I would prefer them to be a fading gradient as they go further.
What I currently have:
What I'd like to achieve:
Right now I'm using paths to draw my shadows on a 2D Canvas. The code that is currently in place is the following:
// Check if edge is invisible from the perspective of origin
var a = points[points.length - 1];
for (var i = 0; i < points.length; ++i, a = b)
{
var b = points[i];
var originToA = _vec2(origin, a);
var normalAtoB = _normal(a, b);
var normalDotOriginToA = _dot(normalAtoB, originToA);
// If the edge is invisible from the perspective of origin it casts
// a shadow.
if (normalDotOriginToA < 0)
{
// dot(a, b) == cos(phi) * |a| * |b|
// thus, dot(a, b) < 0 => cos(phi) < 0 => 90° < phi < 270°
var originToB = _vec2(origin, b);
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(a.x + scale * originToA.x,
a.y + scale * originToA.y);
ctx.lineTo(b.x + scale * originToB.x,
b.y + scale * originToB.y);
ctx.lineTo(b.x, b.y);
ctx.closePath();
ctx.globalAlpha = _shadowIntensity / 2;
ctx.fillStyle = 'black';
ctx.fillRect(_innerX, _innerY, _innerWidth, _innerHeight);
ctx.globalAlpha = _shadowIntensity;
ctx.fill();
ctx.globalAlpha = 1;
}
}
Suggestions on how I could go about achieving this? Any and all help is highly appreciated.
You can use composition + the new filter property on the context which takes CSS filters, in this case blur.
You will have to do it in several steps - normally this falls under the 3D domain, but we can "fake" it in 2D as well by rendering a shadow-map.
Here we render a circle shape along a line represented by length and angle, number of iterations, where each iteration increasing the blur radius. The strength of the shadow is defined by its color and opacity.
If the filter property is not available in the browser it can be replaced by a manual blur (there are many out there such as StackBoxBlur and my own rtblur), or simply use a radial gradient.
For multiple use and speed increase, "cache" or render to an off-screen canvas and when done composite back to the main canvas. This will require you to calculate the size based on max blur radius as well as initial radius, then render it centered at angle 0°. To draw use drawImage() with a local transform transformed based on start of shadow, then rotate and scale (not shown below as being a bit too broad).
In the example below it is assumed that the main object is drawn on top after the shadow has been rendered.
The main function takes the following arguments:
renderShadow(ctx, x, y, radius, angle, length, blur, iterations)
// ctx - context to use
// x/y - start of shadow
// radius - shadow radius (assuming circle shaped)
// angle - angle in radians. 0° = right
// length - core-length in pixels (radius/blur adds to real length)
// blur - blur radius in pixels. End blur is radius * iterations
// iterations - line "resolution"/quality, also affects total end blur
Play around with shape, shadow color, blur radius etc. to find the optimal result for your scene.
Demo
Result if browser supports filter:
var ctx = c.getContext("2d");
// render shadow
renderShadow(ctx, 30, 30, 30, Math.PI*0.25, 300, 2.5, 20);
// show main shape
ctx.beginPath();
ctx.moveTo(60, 30);
ctx.arc(30, 30, 30, 0, 6.28);
ctx.fillStyle = "rgb(0,140,200)";
ctx.fill();
function renderShadow(ctx, x, y, radius, angle, length, blur, iterations) {
var step = length / iterations, // calc number of steps
stepX = step * Math.cos(angle), // calc angle step for x based on steps
stepY = step * Math.sin(angle); // calc angle step for y based on steps
for(var i = iterations; i > 0; i--) { // run number of iterations
ctx.beginPath(); // create some shape, here circle
ctx.moveTo(x + radius + i * stepX, y + i * stepY); // move to x/y based on step*ite.
ctx.arc(x + i * stepX, y + i * stepY, radius, 0, 6.28);
ctx.filter = "blur(" + (blur * i) + "px)"; // set filter property
ctx.fillStyle = "rgba(0,0,0,0.5)"; // shadow color
ctx.fill();
}
ctx.filter = "none"; // reset filter
}
<canvas id=c width=450 height=350></canvas>

Black resized canvas not completely fading drawings to black over time

I have a black canvas with things being drawn inside it. I want the things drawn inside to fade to black, over time, in the order at which they are drawn (FIFO). This works if I use a canvas which hasn't been resized. When the canvas is resized, the elements fade to an off-white.
Question: Why don't the white specks fade completely to black when the canvas has been resized? How can I get them to fade to black in the same way that they do when I haven't resized the canvas?
Here's some code which demonstrates. http://jsfiddle.net/6VvbQ/35/
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0, 0, 300, 150);
// Comment this out and it works as intended, why?
canvas.width = canvas.height = 300;
window.draw = function () {
context.fillStyle = 'rgba(255,255,255,1)';
context.fillRect(
Math.floor(Math.random() * 300),
Math.floor(Math.random() * 150),
2, 2);
context.fillStyle = 'rgba(0,0,0,.02)';
context.fillRect(0, 0, 300, 150);
setTimeout('draw()', 1000 / 20);
}
setTimeout('draw()', 1000 / 20);
The problem is two-parted:
There is a (rather known) rounding error when you draw with low alpha value. The browser will never be able to get the resulting mix of the color and alpha channel equal to 0 as the resulting float value that is mixed will be converted to integer at the time of drawing which means the value will never become lower than 1. Next time it mixes it (value 1, as alpha internally is a value between 0 and 255) will use this value again and it get rounded to again to 1, and forever it goes.
Why it works when you have a resized canvas - in this case it is because you are drawing only half the big canvas to the smaller which result in the pixels being interpolated. As the value is very low this means in this case the pixel will turn "black" (fully transparent) as the average between the surrounding pixels will result in the value being rounded to 0 - sort of the opposite than with #1.
To get around this you will manually have to clear the spec when it is expected to be black. This will involve tracking each particle/spec yourselves or change the alpha using direct pixel manipulation.
Update:
The key is to use tracking. You can do this by creating each spec as a self-updating point which keeps track of alpha and clearing.
Online demo here
A simple spec object can look like this:
function Spec(ctx, speed) {
var me = this;
reset(); /// initialize object
this.update = function() {
ctx.clearRect(me.x, me.y, 1, 1); /// clear previous drawing
this.alpha -= speed; /// update alpha
if (this.alpha <= 0) reset(); /// if black then reset again
/// draw the spec
ctx.fillStyle = 'rgba(255,255,255,' + me.alpha + ')';
ctx.fillRect(me.x, me.y, 1, 1);
}
function reset() {
me.x = (ctx.canvas.width * Math.random())|0; /// random x rounded to int
me.y = (ctx.canvas.height * Math.random())|0; /// random y rounded to int
if (me.alpha) { /// reset alpha
me.alpha = 1.0; /// set to 1 if existed
} else {
me.alpha = Math.random(); /// use random if not
}
}
}
Rounding the x and y to integer values saves us a little when we need to clear the spec as we won't run into sub-pixels. Otherwise you would need to clear the area around the spec as well.
The next step then is to generate a number of points:
/// create 100 specs with random speed
var i = 100, specs = [];
while(i--) {
specs.push(new Spec(ctx, Math.random() * 0.015 + 0.005));
}
Instead of messing with FPS you simply use the speed which can be set individually per spec.
Now it's simply a matter of updating each object in a loop:
function loop() {
/// iterate each object
var i = specs.length - 1;
while(i--) {
specs[i].update(); /// update each object
}
requestAnimationFrame(loop); /// loop synced to monitor
}
As you can see performance is not an issue and there is no residue left. Hope this helps.
I don't know if i have undertand you well but looking at you fiddle i think that, for what you are looking for, you need to provide the size of the canvas in any iteration of the loop. If not then you are just taking the initial values:
EDIT
You can do it if you apply a threshold filter to the canvas. You can run the filter every second only just so the prefromanece is not hit so hard.
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0,0,300,150);
//context.globalAlpha=1;
//context.globalCompositeOperation = "source-over";
var canvas2 = document.getElementById('canvas2');
var context2 = canvas2.getContext('2d');
canvas2.width=canvas2.height=canvas.width;
window.draw = function(){
var W = canvas2.width;
var H = canvas2.height;
context2.fillStyle='rgba(255,255,255,1)';
context2.fillRect(
Math.floor(Math.random()*W),
Math.floor(Math.random()*H),
2,2);
context2.fillStyle='rgba(0,0,0,.02)';
context2.fillRect(0,0,W,H);
context.fillStyle='rgba(0,0,0,1)';
context.fillRect(0,0,300,150);
context.drawImage(canvas2,0,0,300,150);
setTimeout('draw()', 1000/20);
}
setTimeout('draw()', 1000/20);
window.thresholdFilter = function () {
var W = canvas2.width;
var H = canvas2.height;
var i, j, threshold = 30, rgb = []
, imgData=context2.getImageData(0,0,W,H), Npixels = imgData.data.length;
for (i = 0; i < Npixels; i += 4) {
rgb[0] = imgData.data[i];
rgb[1] = imgData.data[i+1];
rgb[2] = imgData.data[i+2];
if ( rgb[0] < threshold &&
rgb[1] < threshold &&
rgb[2] < threshold
) {
imgData.data[i] = 0;
imgData.data[i+1] = 0;
imgData.data[i+2] = 0;
}
}
context2.putImageData(imgData,0,0);
};
setInterval("thresholdFilter()", 1000);
Here is the fiddle: http://jsfiddle.net/siliconball/2VaLb/4/
To avoid the rounding problem you could extract the fade effect to a separate function with its own timer, using longer refresh interval and larger alpha value.
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0, 0, 300, 150);
// Comment this out and it works as intended, why?
canvas.width = canvas.height = 300;
window.draw = function () {
context.fillStyle = 'rgba(255,255,255,1)';
context.fillRect(
Math.floor(Math.random() * 300),
Math.floor(Math.random() * 300),
2, 2);
setTimeout('draw()', 1000 / 20);
}
window.fadeToBlack = function () {
context.fillStyle = 'rgba(0,0,0,.1)';
context.fillRect(0, 0, 300, 300);
setTimeout('fadeToBlack()', 1000 / 4);
}
draw();
fadeToBlack();
Fiddle demonstrating this: http://jsfiddle.net/6VvbQ/37/

Categories

Resources