I'm drawing an image onto a canvas using drawImage. It's a PNG that is surrounded by transparent pixels, like this:
How can I add a solid-colored border to the visible part of that image on the canvas? To clarify: I don't want a rectangle that surrounds the image's bounding box. The border should go around the grass patch.
I did consider using shadows, but I don't really want a glowing border, I want a solid one.
A bit late, but just draw the image offset which is much faster than analyzing the edges:
var ctx = canvas.getContext('2d'),
img = new Image;
img.onload = draw;
img.src = "http://i.stack.imgur.com/UFBxY.png";
function draw() {
var dArr = [-1,-1, 0,-1, 1,-1, -1,0, 1,0, -1,1, 0,1, 1,1], // offset array
s = 2, // thickness scale
i = 0, // iterator
x = 5, // final position
y = 5;
// draw images at offsets from the array scaled by s
for(; i < dArr.length; i += 2)
ctx.drawImage(img, x + dArr[i]*s, y + dArr[i+1]*s);
// fill with color
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = "red";
ctx.fillRect(0,0,canvas.width, canvas.height);
// draw original image in normal mode
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(img, x, y);
}
<canvas id=canvas width=500 height=500></canvas>
==> ==>
First, attributions:
As #Philipp says, you'll need to analyze pixel data to get your outline border.
You can use the "Marching Squares" algorithm to determine which transparent pixels border the non-transparent grass pixels. You can read more about the Marching Squares algorithm here: http://en.wikipedia.org/wiki/Marching_squares
Michael Bostock has a very nice plugin version of Marching Squares in his d3 data visualization application (IMHO, d3 is the best open-source data visualization program available). Here's a link to the plugin: https://github.com/d3/d3-plugins/tree/master/geom/contour
You can outline the border of your grass image like this:
Draw your image on the canvas
Grab the image's pixel data using .getImageData
Configure the plug-in to look for transparent pixels bordering opaque pixels
// This is used by the marching ants algorithm
// to determine the outline of the non-transparent
// pixels on the image using pixel data
var defineNonTransparent=function(x,y){
var a=data[(y*cw+x)*4+3];
return(a>20);
}
Call the plugin which returns a set of points which outline the border of your image.
// call the marching ants algorithm
// to get the outline path of the image
// (outline=outside path of transparent pixels
points=geom.contour(defineNonTransparent);
Use the set of points to draw a path around your image.
Here's annotated code and a Demo:
// Marching Squares Edge Detection
// this is a "marching ants" algorithm used to calc the outline path
(function() {
// d3-plugin for calculating outline paths
// License: https://github.com/d3/d3-plugins/blob/master/LICENSE
//
// Copyright (c) 2012-2014, Michael Bostock
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//* Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//* Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//* The name Michael Bostock may not be used to endorse or promote products
// derived from this software without specific prior written permission.
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
geom = {};
geom.contour = function(grid, start) {
var s = start || d3_geom_contourStart(grid), // starting point
c = [], // contour polygon
x = s[0], // current x position
y = s[1], // current y position
dx = 0, // next x direction
dy = 0, // next y direction
pdx = NaN, // previous x direction
pdy = NaN, // previous y direction
i = 0;
do {
// determine marching squares index
i = 0;
if (grid(x-1, y-1)) i += 1;
if (grid(x, y-1)) i += 2;
if (grid(x-1, y )) i += 4;
if (grid(x, y )) i += 8;
// determine next direction
if (i === 6) {
dx = pdy === -1 ? -1 : 1;
dy = 0;
} else if (i === 9) {
dx = 0;
dy = pdx === 1 ? -1 : 1;
} else {
dx = d3_geom_contourDx[i];
dy = d3_geom_contourDy[i];
}
// update contour polygon
if (dx != pdx && dy != pdy) {
c.push([x, y]);
pdx = dx;
pdy = dy;
}
x += dx;
y += dy;
} while (s[0] != x || s[1] != y);
return c;
};
// lookup tables for marching directions
var d3_geom_contourDx = [1, 0, 1, 1,-1, 0,-1, 1,0, 0,0,0,-1, 0,-1,NaN],
d3_geom_contourDy = [0,-1, 0, 0, 0,-1, 0, 0,1,-1,1,1, 0,-1, 0,NaN];
function d3_geom_contourStart(grid) {
var x = 0,
y = 0;
// search for a starting point; begin at origin
// and proceed along outward-expanding diagonals
while (true) {
if (grid(x,y)) {
return [x,y];
}
if (x === 0) {
x = y + 1;
y = 0;
} else {
x = x - 1;
y = y + 1;
}
}
}
})();
//////////////////////////////////////////
// canvas related variables
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
// checkbox to show/hide the original image
var $showImage=$("#showImage");
$showImage.prop('checked', true);
// checkbox to show/hide the path outline
var $showOutline=$("#showOutline");
$showOutline.prop('checked', true);
// an array of points that defines the outline path
var points;
// pixel data of this image for the defineNonTransparent
// function to use
var imgData,data;
// This is used by the marching ants algorithm
// to determine the outline of the non-transparent
// pixels on the image
var defineNonTransparent=function(x,y){
var a=data[(y*cw+x)*4+3];
return(a>20);
}
// load the image
var img=new Image();
img.crossOrigin="anonymous";
img.onload=function(){
// draw the image
// (this time to grab the image's pixel data
ctx.drawImage(img,canvas.width/2-img.width/2,canvas.height/2-img.height/2);
// grab the image's pixel data
imgData=ctx.getImageData(0,0,canvas.width,canvas.height);
data=imgData.data;
// call the marching ants algorithm
// to get the outline path of the image
// (outline=outside path of transparent pixels
points=geom.contour(defineNonTransparent);
ctx.strokeStyle="red";
ctx.lineWidth=2;
$showImage.change(function(){ redraw(); });
$showOutline.change(function(){ redraw(); });
redraw();
}
img.src="http://i.imgur.com/QcxIJxa.png";
// redraw the canvas
// user determines if original-image or outline path or both are visible
function redraw(){
// clear the canvas
ctx.clearRect(0,0,canvas.width,canvas.height);
// draw the image
if($showImage.is(':checked')){
ctx.drawImage(img,canvas.width/2-img.width/2,canvas.height/2-img.height/2);
}
// draw the path (consisting of connected points)
if($showOutline.is(':checked')){
// draw outline path
ctx.beginPath();
ctx.moveTo(points[0][0],points[0][4]);
for(var i=1;i<points.length;i++){
var point=points[i];
ctx.lineTo(point[0],point[1]);
}
ctx.closePath();
ctx.stroke();
}
}
body{ background-color: ivory; }
canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<input type="checkbox" id="showImage" />Show Image<br>
<input type="checkbox" id="showOutline" />Show Outline Path<br>
<canvas id="canvas" width=300 height=450></canvas>
I was looking for a way to do this and it seems there are only laborious solutions.
I came up with a little workaround using shadows and a loop to display them all around the image:
// Shadow color and blur
// To get a blurry effect use rgba() with a low opacity as it will be overlaid
context.shadowColor = "red";
context.shadowBlur = 0;
// X offset loop
for(var x = -2; x <= 2; x++){
// Y offset loop
for(var y = -2; y <= 2; y++){
// Set shadow offset
context.shadowOffsetX = x;
context.shadowOffsetY = y;
// Draw image with shadow
context.drawImage(img, left, top, width, height);
}
}
Related
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.
How is it / is it possible to draw using the mouse a canvas using 3 axis(x,y,z).
I know that one can draw a canvas on 2 axis and I have done that successfully.
But I have no idea of how I shall draw it on 3 axis (for example a cube).
Following shows some 2d canvas drawing functionallity
$(canvas).on('mousemove', function(e) {
mousex = parseInt(e.clientX-canvasx);
mousey = parseInt(e.clientY-canvasy);
if(mousedown) {
ctx.beginPath();
if(tooltype=='draw') {
ctx.globalCompositeOperation = 'source-over';
ctx.strokeStyle = 'black';
ctx.lineWidth = 3;
} else {
ctx.globalCompositeOperation = 'destination-out';
ctx.lineWidth = 10;
}
ctx.moveTo(last_mousex,last_mousey);
ctx.lineTo(mousex,mousey);
ctx.lineJoin = ctx.lineCap = 'round';
ctx.stroke();
}
last_mousex = mousex;
last_mousey = mousey;
//Output
$('#output').html('current: '+mousex+', '+mousey+'<br/>last: '+last_mousex+', '+last_mousey+'<br/>mousedown: '+mousedown);
});
The full code https://jsfiddle.net/ArtBIT/kneDX/.
But how can I add a z axis and draw a 3d canvas for instance a cube.
With 2D it is simple, you have the X and Y coordinate of the mouse, and when a mouse button is clicked you can change pixels at that location in the canvas.
3D on the other hand is quite hard. Because of the extra dimension that does not exist on the 2D surface, you need to know how to control the 3D positions. And to make matters worse, with that third dimension comes all kinds of extra's that everyone likes to have: lightning and shadows, effects, focus, etc.
Simple drawing
In its most basic form, (set aside some arithmic) you can flatten the Z axis on the 2D surface with a single division. Suppose that you have a point in 3D which consists of three points on three axis (x3d, y3d, z3d) then you can do:
var x2d = x3d / z3d;
var y2d = y3d / z3d;
If you're new to 3D, you will want to play with this first. Here is a tutorial.
Advanced drawing
For just particles and lines this is rather straightforward, although you might want to use another perspective. But it gets more complicated soon when you use objects and want to rotate them in 3D space. This is why most people rely on an engine like three.js to do the 3D drawing for them.
Control 3D space
When drawing with the mouse, you need to map the 2D mouse movement to 3D for control. For examples, have a look a these 3D GUI's: Microsoft's Paint 3D, Google's Sketchup, and Blender. Note that the more kinds of mappings needs to be implemented (like scaling and other transformations) the more math is required.
Using three.js would help you out. See here: https://jsfiddle.net/bn890dtc/
The core code for drawing the line as your click and drag:
function onMouseMove(evt) {
if (renderer) {
var x = (event.clientX / window.innerWidth) * 2 - 1;
var y = -(event.clientY / window.innerHeight) * 2 + 1;
var z = 0
var vNow = new THREE.Vector3(x, y, z);
vNow.unproject(camera);
splineArray.push(vNow);
}
}
The line
vNow.unproject(camera);
will project your drawing into 3D space.
This function will update the line in 3D space:
function updatePositions() {
var positions = line.geometry.attributes.position.array;
var index = 0;
for ( var i = 0; i < splineArray.length; i ++ ) {
positions[ index ++ ] = splineArray[i].x;
positions[ index ++ ] = splineArray[i].y;
positions[ index ++ ] = splineArray[i].z;
}
}
My game has many Laser objects. mx & my represent velocity. I use the following code to draw a line from behind the Laser 2 pixels to ahead of the Laser in the direction it's going 2 pixels.
Removing the first line of the function adjusted the % of the Profiling by ~1% but I don't like the way it looks. I think I could optimize the drawing by sorting by Linewidth but that doesn't appear to get me much.
How else could I optimize this?
Laser.prototype.draw = function(client, context) {
context.lineWidth = Laser.lineWidth;
context.beginPath();
context.moveTo(this.x - this.mx * 2, this.y - this.my * 2);
context.lineTo(this.x + this.mx * 2, this.y + this.my * 2);
context.strokeStyle = this.teamColor;
context.closePath();
context.stroke();
}
Instead of multiplying things by two, why not add them?
E.g.
context.moveTo(this.x - this.mx - this.mx, this.y - this.my - this.my);
context.lineTo(this.x + this.mx + this.mx, this.y + this.my - this.my);
Testing shows that addition is an order of magnitude faster on an imac over multiplication
https://jsfiddle.net/1c85r2pq/
Dont use moveTo or lineTo as they do not use the hardware to render and are very slow. Also your code is drawing the line twice
ctx.beginPath(); // starts a new path
ctx.moveTo(x,y); // sets the start point of a line
ctx.lineTo(xx,yy); // add a line from x,y to xx,yy
// Not needed
ctx.closePath(); // This is not like beginPath
// it is like lineTo and tells the context
// to add a line from the last point xx,yy
// back to the last moveTo which is x,y
This would half the already slow render time.
A quick way to draw lines using bitmaps.
First at the start create an image to hold the bitmap used to draw the line
function createLineSprite(col,width){
var lineSprite = document.createElement("canvas");
var lineSprite.width = 2;
var lineSprite.height = width;
lineSprite.ctx = lineSprite.getContext("2d");
lineSprite.ctx.fillStyle = col;
lineSprite.ctx.fillRect(0,0,2,width);
return lineSprite;
}
var line = createLineSprite("red",4); // create a 4 pixel wide red line sprite
Or you can use an image that you load.
To draw a line you just need to create a transform that points in the direction of the line, and draw that sprite the length of the line.
// draw a line with sprite from x,y,xx,yy
var drawLineSprite = function(sprite,x,y,xx,yy){
var nx = xx-x; // get the vector between the points
var ny = yy-y;
if(nx === 0 && ny === 0){ // nothing to draw
return;
}
var d = Math.hypot(nx,ny); // get the distance. Note IE does not have hypot Edge does
// normalise the vector
nx /= d;
ny /= d;
ctx.setTransform(nx,ny,-ny,nx,x,y); // create the transform with x axis
// along the line and origin at line start x,y
ctx.drawImage(sprite, 0, 0, sprite.width, sprite.height, 0, -sprite.height / 2, d, sprite.height);
}
To draw the line
drawSpriteLine(line,0,0,100,100);
When you are done drawing all the lines you can get the default transform back with
ctx.setTransform(1,0,0,1,0,0);
The sprite can be anything, this allows for very detailed lines and great for game lasers and the like.
If you have many different colours to draw then create one sprite (image) that has many colour on it, then in the line draw function simply draw only the part of the sprite that has the colour you want. You can stretch out a single pixel to any size so you can get many colours on a small bitmap.
I have these event listeners in my code
canvas.addEventListener('mousemove', onMouseMove, false);
canvas.addEventListener('mousedown', onMouseDown,false);
canvas.addEventListener('mouseup', onMouseUp, false);
These functions will help me to pan the canvas. I have declared a variable in the onLoad called pan, isDown, mousePostion and previous mouse positions. Then in the initialise function is set the pan,mousePos and premousepos to vectors containing 0,0
function draw() {
context.translate(pan.getX(), pan.getY());
topPerson.draw(context);
console.log(pan);
}
function onMouseDown(event) {
var x = event.offsetX;
var y = event.offsetY;
var mousePosition = new vector(event.offsetX, event.offsetY);
previousMousePosition = mousePosition;
isDown = true;
console.log(previousMousePosition);
console.log("onmousedown" + "X coords: " + x + ", Y coords: " + y);
}
function onMouseUp(event) {
isDown = false;
}
function onMouseMove(event) {
if (isDown) {
console.log(event.offsetX);
mousePosition = new vector(event.offsetX, event.offsetY);
newMousePosition = mousePosition;
console.log('mouseMove' + newMousePosition);
var panX = newMousePosition.getX() - previousMousePosition.getX();
var panY = newMousePosition.getY() - previousMousePosition.getY();
console.log('onMouseMove: ' + panX);
pan = new vector(panX, panY);
console.log('mouseMove' + pan);
}
}
But it is not registering the new pan Values so you could attempt to drag the canvas. I know my mouse dragging events work but is just doesnt pan.
Here's a simple (annotated) example of panning code
It works by accumulating the net amount the mouse has been dragged horizontally (and vertically) Then it redraws everything, but offset by those accumulated horizontal & vertical distances.
Example code and a Demo:
// canvas related variables
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
// account for scrolling
function reOffset(){
var BB=canvas.getBoundingClientRect();
offsetX=BB.left;
offsetY=BB.top;
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
window.onresize=function(e){ reOffset(); }
// mouse drag related variables
var isDown=false;
var startX,startY;
// the accumulated horizontal(X) & vertical(Y) panning the user has done in total
var netPanningX=0;
var netPanningY=0;
// just for demo: display the accumulated panning
var $results=$('#results');
// draw the numbered horizontal & vertical reference lines
for(var x=0;x<100;x++){ ctx.fillText(x,x*20,ch/2); }
for(var y=-50;y<50;y++){ ctx.fillText(y,cw/2,y*20); }
// listen for mouse events
$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
$("#canvas").mouseup(function(e){handleMouseUp(e);});
$("#canvas").mouseout(function(e){handleMouseOut(e);});
function handleMouseDown(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// calc the starting mouse X,Y for the drag
startX=parseInt(e.clientX-offsetX);
startY=parseInt(e.clientY-offsetY);
// set the isDragging flag
isDown=true;
}
function handleMouseUp(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// clear the isDragging flag
isDown=false;
}
function handleMouseOut(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// clear the isDragging flag
isDown=false;
}
function handleMouseMove(e){
// only do this code if the mouse is being dragged
if(!isDown){return;}
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// get the current mouse position
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// dx & dy are the distance the mouse has moved since
// the last mousemove event
var dx=mouseX-startX;
var dy=mouseY-startY;
// reset the vars for next mousemove
startX=mouseX;
startY=mouseY;
// accumulate the net panning done
netPanningX+=dx;
netPanningY+=dy;
$results.text('Net change in panning: x:'+netPanningX+'px, y:'+netPanningY+'px');
// display the horizontal & vertical reference lines
// The horizontal line is offset leftward or rightward by netPanningX
// The vertical line is offset upward or downward by netPanningY
ctx.clearRect(0,0,cw,ch);
for(var x=-50;x<50;x++){ ctx.fillText(x,x*20+netPanningX,ch/2); }
for(var y=-50;y<50;y++){ ctx.fillText(y,cw/2,y*20+netPanningY); }
}
body{ background-color: ivory; }
#canvas{border:1px solid red; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4 id=results>Drag the mouse to see net panning in x,y directions</h4>
<canvas id="canvas" width=300 height=150></canvas>
To answer question
You have not provided some of the code. Specifically the vector object you are creating each event, could be there. (really you should not be creating a new object each time. Create once and update the values)
What I do see is that mouseMove events do not update the previous mouse position object so you will only get panning from the last mouse down. But you may want that. So without the code I don't know what is wrong as the code given is OK.
Below is how I do the whole shabang..
How to pan (and zoom).
Below is an example of panning and zooming with the mouse. Its a little more complex than standard pan and zooms, that is because I have added some smoothing to the pan and zoom to give it a better interactive feel.
How it works.
The canvas uses a transformation matrix to transform points. What this does is maintain that matrix. I call the transformed space, real space. I also maintain an inverse matrix, that is used to convert from screen space into real space.
The core of the demo is around the object displayTransform it holds the matrix, all the individual values needed, and the functions update() call once a frame, setHome() get the screen space transform and applies it to the canvas. Used to clear the screen. And setTransform() this set the canvas to real space (the zoomed panned space)
To smooth out movements I have a mirror of the values x, y, ox, oy, scale, and rotate. ((ox,oy) are origin x and y) (and yes rotate works) each of these variable has a delta prefixed with d and a chaser prefixed with c. The chaser values chase the required values. You should not touch the chaser values. There are two values called drag and accel (short for acceleration) drag (not real simulated drag) is how quickly the deltas decay. Values for drag > 0.5 will result in a bouncy response. As you get toward one it will get more and more bouncy. At 1 the bound will not stop, above one and it's unusable. 'accel' is how quickly the transform responds to mouse movement. Low values are slow response, 0 is no response at all, and one is instant response. Play with the values to find what you like.
Example of the logic for chaser values
var x = 100; // the value to be chased
var dx = 0; // the delta x or the change in x per frame
var cx = 0; // the chaser value. This value chases x;
var drag = 0.1; // quick decay
var accel = 0.9; // quick follpw
// logic
dx += (x-cx)*accel; // get acceleration towards x
dx *= drag; // apply the drag
cx += dx; // change chaser by delta x.
Convert coords
No point having a zoom panned rotated canvas if you don't know where things are. To do this I keep an inverse matrix. It converts screen x and y into realspace x and y. For convenience I convert the mouse to real space every update. If you want the reverse realSpace to screen space. then its just
var x; // real x coord (position in the zoom panned rotate space)
var y; // real y coord
// "this" is displayTransform
x -= this.cx;
y -= this.cy;
// screenX and screen Y are the screen coordinates.
screenX = (x * this.matrix[0] + y * this.matrix[2])+this.cox;
screenY = (x * this.matrix[1] + y * this.matrix[3])+this.coy;
You can see it at the end of the mouse displayTransform.update where I use the inverse transform to convert the mouse screen coords to real coords. Then in the main update loop I use the mouse real coords to display the help text. I leave it up to the user of the code to create a function that will convert any screen coord. (easy just pinch the bit where the mouse is being converted).
Zoom
The zoom is done with the mouse wheel. This presents a bit of a problem and naturally you expect the zoom to be centered on the mouse. But the transform is actually relative to the top left of the screen. To fix this I also keep an origin x and y. This basically floats about until the wheel zoom is needed then it is set to the mouse real position, and the mouse distance from the top left is placed in the transform x and y position. Then just increase or decrease the scale to zoom in and out. I have left the origin and offset to float (not set the chase values) this works for the current drag and acceleration setting but if you notice that it's not working that well with other setting set the the cx, cy, cox, coy values as well. ( I have added a note in the code)
Pan
Pan is done with the left mouse button. Click and drag to pan. This is straight forward. I get the difference between the last mouse position and the new one screen space (the coords given by the mouse events) This gives me a mouse delta vector. I transform the delta mouse vector into real space and subtract that from the top left coords displayTransform.x and displayTransform.y. Thats it I let the chaser x and y smooth it all out.
The snippet just displays a large image that can be panned and zoomed. I check for the complete flag rather than use onload. While the image is loading the snippet will just display loading. The main loop is refreshed with requestAnimationFrame, first I update the displayTransform then the canvas is cleared in home space (screen space) and then the image is displayed in real space. As always I a fighting time so will return as time permits to add more comments, and maybe a function or two.
If you find the chase variables a little to much, you can just remove them and replace all the c prefixed vars with the unprefixed ones.
OK hope this helps. Not done yet as need to clean up but need to do some real work for a bit.
var canvas = document.getElementById("canV");
var ctx = canvas.getContext("2d");
var mouse = {
x : 0,
y : 0,
w : 0,
alt : false,
shift : false,
ctrl : false,
buttonLastRaw : 0, // user modified value
buttonRaw : 0,
over : false,
buttons : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
};
function mouseMove(event) {
mouse.x = event.offsetX;
mouse.y = event.offsetY;
if (mouse.x === undefined) {
mouse.x = event.clientX;
mouse.y = event.clientY;
}
mouse.alt = event.altKey;
mouse.shift = event.shiftKey;
mouse.ctrl = event.ctrlKey;
if (event.type === "mousedown") {
event.preventDefault()
mouse.buttonRaw |= mouse.buttons[event.which-1];
} else if (event.type === "mouseup") {
mouse.buttonRaw &= mouse.buttons[event.which + 2];
} else if (event.type === "mouseout") {
mouse.buttonRaw = 0;
mouse.over = false;
} else if (event.type === "mouseover") {
mouse.over = true;
} else if (event.type === "mousewheel") {
event.preventDefault()
mouse.w = event.wheelDelta;
} else if (event.type === "DOMMouseScroll") { // FF you pedantic doffus
mouse.w = -event.detail;
}
}
function setupMouse(e) {
e.addEventListener('mousemove', mouseMove);
e.addEventListener('mousedown', mouseMove);
e.addEventListener('mouseup', mouseMove);
e.addEventListener('mouseout', mouseMove);
e.addEventListener('mouseover', mouseMove);
e.addEventListener('mousewheel', mouseMove);
e.addEventListener('DOMMouseScroll', mouseMove); // fire fox
e.addEventListener("contextmenu", function (e) {
e.preventDefault();
}, false);
}
setupMouse(canvas);
// terms.
// Real space, real, r (prefix) refers to the transformed canvas space.
// c (prefix), chase is the value that chases a requiered value
var displayTransform = {
x:0,
y:0,
ox:0,
oy:0,
scale:1,
rotate:0,
cx:0, // chase values Hold the actual display
cy:0,
cox:0,
coy:0,
cscale:1,
crotate:0,
dx:0, // deltat values
dy:0,
dox:0,
doy:0,
dscale:1,
drotate:0,
drag:0.1, // drag for movements
accel:0.7, // acceleration
matrix:[0,0,0,0,0,0], // main matrix
invMatrix:[0,0,0,0,0,0], // invers matrix;
mouseX:0,
mouseY:0,
ctx:ctx,
setTransform:function(){
var m = this.matrix;
var i = 0;
this.ctx.setTransform(m[i++],m[i++],m[i++],m[i++],m[i++],m[i++]);
},
setHome:function(){
this.ctx.setTransform(1,0,0,1,0,0);
},
update:function(){
// smooth all movement out. drag and accel control how this moves
// acceleration
this.dx += (this.x-this.cx)*this.accel;
this.dy += (this.y-this.cy)*this.accel;
this.dox += (this.ox-this.cox)*this.accel;
this.doy += (this.oy-this.coy)*this.accel;
this.dscale += (this.scale-this.cscale)*this.accel;
this.drotate += (this.rotate-this.crotate)*this.accel;
// drag
this.dx *= this.drag;
this.dy *= this.drag;
this.dox *= this.drag;
this.doy *= this.drag;
this.dscale *= this.drag;
this.drotate *= this.drag;
// set the chase values. Chase chases the requiered values
this.cx += this.dx;
this.cy += this.dy;
this.cox += this.dox;
this.coy += this.doy;
this.cscale += this.dscale;
this.crotate += this.drotate;
// create the display matrix
this.matrix[0] = Math.cos(this.crotate)*this.cscale;
this.matrix[1] = Math.sin(this.crotate)*this.cscale;
this.matrix[2] = - this.matrix[1];
this.matrix[3] = this.matrix[0];
// set the coords relative to the origin
this.matrix[4] = -(this.cx * this.matrix[0] + this.cy * this.matrix[2])+this.cox;
this.matrix[5] = -(this.cx * this.matrix[1] + this.cy * this.matrix[3])+this.coy;
// create invers matrix
var det = (this.matrix[0] * this.matrix[3] - this.matrix[1] * this.matrix[2]);
this.invMatrix[0] = this.matrix[3] / det;
this.invMatrix[1] = - this.matrix[1] / det;
this.invMatrix[2] = - this.matrix[2] / det;
this.invMatrix[3] = this.matrix[0] / det;
// check for mouse. Do controls and get real position of mouse.
if(mouse !== undefined){ // if there is a mouse get the real cavas coordinates of the mouse
if(mouse.oldX !== undefined && (mouse.buttonRaw & 1)===1){ // check if panning (middle button)
var mdx = mouse.x-mouse.oldX; // get the mouse movement
var mdy = mouse.y-mouse.oldY;
// get the movement in real space
var mrx = (mdx * this.invMatrix[0] + mdy * this.invMatrix[2]);
var mry = (mdx * this.invMatrix[1] + mdy * this.invMatrix[3]);
this.x -= mrx;
this.y -= mry;
}
// do the zoom with mouse wheel
if(mouse.w !== undefined && mouse.w !== 0){
this.ox = mouse.x;
this.oy = mouse.y;
this.x = this.mouseX;
this.y = this.mouseY;
/* Special note from answer */
// comment out the following is you change drag and accel
// and the zoom does not feel right (lagging and not
// zooming around the mouse
/*
this.cox = mouse.x;
this.coy = mouse.y;
this.cx = this.mouseX;
this.cy = this.mouseY;
*/
if(mouse.w > 0){ // zoom in
this.scale *= 1.1;
mouse.w -= 20;
if(mouse.w < 0){
mouse.w = 0;
}
}
if(mouse.w < 0){ // zoom out
this.scale *= 1/1.1;
mouse.w += 20;
if(mouse.w > 0){
mouse.w = 0;
}
}
}
// get the real mouse position
var screenX = (mouse.x - this.cox);
var screenY = (mouse.y - this.coy);
this.mouseX = this.cx + (screenX * this.invMatrix[0] + screenY * this.invMatrix[2]);
this.mouseY = this.cy + (screenX * this.invMatrix[1] + screenY * this.invMatrix[3]);
mouse.rx = this.mouseX; // add the coordinates to the mouse. r is for real
mouse.ry = this.mouseY;
// save old mouse position
mouse.oldX = mouse.x;
mouse.oldY = mouse.y;
}
}
}
// image to show
var img = new Image();
img.src = "https://upload.wikimedia.org/wikipedia/commons/e/e5/Fiat_500_in_Emilia-Romagna.jpg"
// set up font
ctx.font = "14px verdana";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// timer for stuff
var timer =0;
function update(){
timer += 1; // update timere
// update the transform
displayTransform.update();
// set home transform to clear the screem
displayTransform.setHome();
ctx.clearRect(0,0,canvas.width,canvas.height);
// if the image loaded show it
if(img.complete){
displayTransform.setTransform();
ctx.drawImage(img,0,0);
ctx.fillStyle = "white";
if(Math.floor(timer/100)%2 === 0){
ctx.fillText("Left but to pan",mouse.rx,mouse.ry);
}else{
ctx.fillText("Wheel to zoom",mouse.rx,mouse.ry);
}
}else{
// waiting for image to load
displayTransform.setTransform();
ctx.fillText("Loading image...",100,100);
}
if(mouse.buttonRaw === 4){ // right click to return to homw
displayTransform.x = 0;
displayTransform.y = 0;
displayTransform.scale = 1;
displayTransform.rotate = 0;
displayTransform.ox = 0;
displayTransform.oy = 0;
}
// reaquest next frame
requestAnimationFrame(update);
}
update(); // start it happening
.canC { width:400px; height:400px;}
div {
font-size:x-small;
}
<div>Wait for image to load and use <b>left click</b> drag to pan, and <b>mouse wheel</b> to zoom in and out. <b>Right click</b> to return to home scale and pan. Image is 4000 by 2000 plus so give it time if you have a slow conection. Not the tha help text follows the mouse in real space. Image from wiki commons</div>
<canvas class="canC" id="canV" width=400 height=400></canvas>
I am trying to design a traveling sine wave in JavaScript, but the design appears quite slow. The main bottleneck is the clearRect() for canvas clearing.
How can I solve this?
Also I am drawing the pixel by ctx.fillRect(x, y,1,1), but when I clear using clearRect(x, y,1,1), it leaves some footprints. Instead I have to do clearRect(x, y,5,5) to get proper clearing. What can be the work around?
/******************************/
var x = 0;
var sineval = [];
var offset = 0;
var animFlag;
function init() {
for(var i=0; i<=1000; ++i){
sineval[i] = Math.sin(i*Math.PI/180);
}
// Call the sineWave() function repeatedly every 1 microseconds
animFlag = setInterval(sineWave, 1);
//sineWave();
}
function sineWave()
{ //console.log('Drawing Sine');
var canvas = document.getElementById("canvas");
if (canvas.getContext) {
var ctx = canvas.getContext("2d");
}
for(x=0 ; x<1000 ;++x){
// Find the sine of the angle
//var i = x % 361;
var y = sineval[x+offset];
// If the sine value is positive, map it above y = 100 and change the colour to blue
if(y >= 0)
{
y = 100 - (y-0) * 70;
ctx.fillStyle = "green";
}
// If the sine value is negative, map it below y = 100 and change the colour to red
if( y < 0 )
{
y = 100 + (0-y) * 70;
ctx.fillStyle = "green";
}
// We will use the fillRect method to draw the actual wave. The length and breath of the
if(x == 0) ctx.clearRect(0,y-1,5,5);
else ctx.clearRect(x,y,5,5);
ctx.fillRect(x, y,1,1 /*Math.sin(x * Math.PI/180) * 5, Math.sin(x * Math.PI/180 * 5)*/);
}
offset = (offset > 360) ? 0 : ++offset ;
}
You need to refactor the code a bit:
Move all global variables such as canvas and context outside of the loop function
Inside the loop, clear full canvas at beginning, redraw sine
Use requestAnimationFrame instead of setInterval
Replace fillRect() with rect() and do a single fill() outside the inner for-loop
Using a timeout value of 1 ms will potentially result in blocking the browser, or at least slow it down noticeably. Considering that a monitor update only happens every 16.7ms this will of course be wasted cycles. If you want to reduce/increase the speed of the sine you can reduce/increase the incremental step instead.
In essence:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var sineval = [];
var offset = 0;
init();
function init() {
for (var i = 0; i <= 1000; ++i) {
sineval.push(Math.sin(i * Math.PI / 180));
}
// Call the sineWave() function
sineWave();
}
function sineWave() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.beginPath();
ctx.fillStyle = "green";
// draw positive part of sine wave here
for (var x = 0; x < 1000; x++) {
var y = sineval[x + offset];
if (y >= 0) {
y = 100 - (y - 0) * 70;
ctx.rect(x, y, 2, 2);
}
}
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "red";
// draw negative part of sine wave here
for (var x = 0; x < 1000; x++) {
var y = sineval[x + offset];
if (y < 0) {
y = 100 - (y - 0) * 70;
ctx.rect(x, y, 2, 2);
}
}
ctx.fill();
offset = (offset > 360) ? 0 : ++offset;
requestAnimationFrame(sineWave);
}
<canvas id="canvas" width=800 height=500></canvas>
And of course, if you load the script in <head> you need to wrap it in a window.onload block so canvas element is available. Or simply place the script at the bottom of the page if you haven't already.
A few speedups and odd ends:
In init, set up the sine wave pixel values one time.
Use typed arrays for these since sticking with integers is faster than using floats if possible.
We will manipulate the pixel data directly instead of using fill and clear. To start this, in init we call ctx.getImageData one time. We also just one time max the alpha value of all the pixels since the default 0 value is transparent and we want full opacity at 255.
Use setInterval like before. We want to update the pixels at a steady rate.
Use 'adj' as knob to adjust how fast the sine wave moves on the screen. The actual value (a decimal) will depend on the drawing frame rate. We use Date.now() calls to keep track of milliseconds consumed across frames. So the adjustment on the millisecond is mod 360 to set the 'offset' variable. Thus offset value is not inc by 1 every frame but instead is decided based on the consumption of time. The adj value could later be connected to gui if want.
At end of work (in sineWave function), we call requestAnimationFrame simply to do the ctx.putImageData to the canvas,screen in sync to avoid tearing. Notice 'paintit' function is fast and simple. Notice also that we still require setInterval to keep steady pace.
In between setting the offset and calling requestAnimationFrame, we do two loops. The first efficiently blackens out the exact pixels we drew from the prior frame (sets to 0). The second loop draws the new sine wave. Top half of wave is green (set the G in pixel rgba to 255). Bottom half is red (set the R pixel rgba to 255).
Use the .data array to paint a pixel, and index it to the pixel using 4x + 4y*canvas.width. Add 1 more if want the green value instead of the red one. No need to touch the blue value (byte offset 2) nor the already set alpha (byte offset 3).
The >>>0 used in some places turns the affected value into an unsigned integer if it wasn't already. It can also be used instead of Math.ceil. .data is typed Array already I think.
This answer is rather late but it addresses some issues brought up in comments or otherwise not yet addressed. The question showed up during googling.
Code hasn't been profiled. It's possible some of the speedups didn't speed anything up; however, the cpu consumption of firefox was pretty light by the end of the adjustments. It's set to run at 40 fps. Make 'delay' smaller to speed it up and tax cpu more.
var sineval;
var offset = 0;
var animFlag;
var canvas;
var ctx;
var obj;
var milli;
var delay=25;
var adj=1/delay; // .04 or so for 25 delay
function init() {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
obj=ctx.getImageData(0,0,canvas.width,canvas.height);
for (let i=0; i<obj.data.length; i+=4) {
obj.data[i+3]=255; //set all alpha to full one time only needed.
}
sineval=new Uint8Array(1400); //set up byte based table of final pixel sine values.. 1400 degrees total
for (let i=0; i<=1400; ++i) { //1400
sineval[i] = (100-70*Math.sin(i*Math.PI/180))>>>0;
}
animFlag = setInterval(sineWave, delay); //do processing once every 25 milli
milli=Date.now()>>>0; //start time in milli
}
function sineWave() {
let m=((Date.now()-milli)*adj)>>>0;
let oldoff = offset;
offset=(m % 360)>>>0; //offset,frequency tuned with adj param.
for(x=0 ; x<1000 ;++x) { //draw sine wave across canvas length of 1000
let y=sineval[x+oldoff];
obj.data [0+x*4+y*4*canvas.width]=0; //black the reds
obj.data [1+x*4+y*4*canvas.width]=0; //black the greens
}
for(x=0 ; x<1000 ;++x) { //draw sine wave across canvas length of 1000
let y=sineval[x+offset];
if (y<100) {
obj.data [1+x*4+y*4*canvas.width]=255; //rGba //green for top half
} else {
obj.data [0+x*4+y*4*canvas.width]=255; //Rgba //red for bottom half
}
}
requestAnimationFrame(paintit); //at end of processing try to paint next frame boundary
}
function paintit() {
ctx.putImageData(obj,0,0);
}
init();
<canvas id="canvas" height=300 width=1000></canvas>