Ask for microphone on onclick event - javascript

The other day I stumbled upon with this example of a Javascript audio recorder:
http://webaudiodemos.appspot.com/AudioRecorder/index.html
Which I ended up using for implementing my own. The problem I'm having is that in this file:
var audioContext = new webkitAudioContext();
var audioInput = null,
realAudioInput = null,
inputPoint = null,
audioRecorder = null;
var rafID = null;
var analyserContext = null;
var canvasWidth, canvasHeight;
var recIndex = 0;
/* TODO:
- offer mono option
- "Monitor input" switch
*/
function saveAudio() {
audioRecorder.exportWAV( doneEncoding );
}
function drawWave( buffers ) {
var canvas = document.getElementById( "wavedisplay" );
drawBuffer( canvas.width, canvas.height, canvas.getContext('2d'), buffers[0] );
}
function doneEncoding( blob ) {
Recorder.forceDownload( blob, "myRecording" + ((recIndex<10)?"0":"") + recIndex + ".wav" );
recIndex++;
}
function toggleRecording( e ) {
if (e.classList.contains("recording")) {
// stop recording
audioRecorder.stop();
e.classList.remove("recording");
audioRecorder.getBuffers( drawWave );
} else {
// start recording
if (!audioRecorder)
return;
e.classList.add("recording");
audioRecorder.clear();
audioRecorder.record();
}
}
// this is a helper function to force mono for some interfaces that return a stereo channel for a mono source.
// it's not currently used, but probably will be in the future.
function convertToMono( input ) {
var splitter = audioContext.createChannelSplitter(2);
var merger = audioContext.createChannelMerger(2);
input.connect( splitter );
splitter.connect( merger, 0, 0 );
splitter.connect( merger, 0, 1 );
return merger;
}
function toggleMono() {
if (audioInput != realAudioInput) {
audioInput.disconnect();
realAudioInput.disconnect();
audioInput = realAudioInput;
} else {
realAudioInput.disconnect();
audioInput = convertToMono( realAudioInput );
}
audioInput.connect(inputPoint);
}
function cancelAnalyserUpdates() {
window.webkitCancelAnimationFrame( rafID );
rafID = null;
}
function updateAnalysers(time) {
if (!analyserContext) {
var canvas = document.getElementById("analyser");
canvasWidth = canvas.width;
canvasHeight = canvas.height;
analyserContext = canvas.getContext('2d');
}
// analyzer draw code here
{
var SPACING = 3;
var BAR_WIDTH = 1;
var numBars = Math.round(canvasWidth / SPACING);
var freqByteData = new Uint8Array(analyserNode.frequencyBinCount);
analyserNode.getByteFrequencyData(freqByteData);
analyserContext.clearRect(0, 0, canvasWidth, canvasHeight);
analyserContext.fillStyle = '#F6D565';
analyserContext.lineCap = 'round';
var multiplier = analyserNode.frequencyBinCount / numBars;
// Draw rectangle for each frequency bin.
for (var i = 0; i < numBars; ++i) {
var magnitude = 0;
var offset = Math.floor( i * multiplier );
// gotta sum/average the block, or we miss narrow-bandwidth spikes
for (var j = 0; j< multiplier; j++)
magnitude += freqByteData[offset + j];
magnitude = magnitude / multiplier;
var magnitude2 = freqByteData[i * multiplier];
analyserContext.fillStyle = "hsl( " + Math.round((i*360)/numBars) + ", 100%, 50%)";
analyserContext.fillRect(i * SPACING, canvasHeight, BAR_WIDTH, -magnitude);
}
}
rafID = window.webkitRequestAnimationFrame( updateAnalysers );
}
function gotStream(stream) {
// "inputPoint" is the node to connect your output recording to.
inputPoint = audioContext.createGainNode();
// Create an AudioNode from the stream.
realAudioInput = audioContext.createMediaStreamSource(stream);
audioInput = realAudioInput;
audioInput.connect(inputPoint);
// audioInput = convertToMono( input );
analyserNode = audioContext.createAnalyser();
analyserNode.fftSize = 2048;
inputPoint.connect( analyserNode );
audioRecorder = new Recorder( inputPoint );
zeroGain = audioContext.createGainNode();
zeroGain.gain.value = 0.0;
inputPoint.connect( zeroGain );
zeroGain.connect( audioContext.destination );
updateAnalysers();
}
function initAudio() {
if (!navigator.webkitGetUserMedia)
return(alert("Error: getUserMedia not supported!"));
navigator.webkitGetUserMedia({audio:true}, gotStream, function(e) {
alert('Error getting audio');
console.log(e);
});
}
window.addEventListener('load', initAudio );
As you might be able to see, the initAudio() function (the one wich ask the user for permission to use his/her microphone) is called inmediately when the page is loaded (read the last line) with this method:
window.addEventListener('load', initAudio );
Now, I have this code in the HTML:
<script type="text/javascript" >
$(function() {
$("#recbutton").on("click", function() {
$("#entrance").hide();
$("#live").fadeIn("slow");
toggleRecording(this);
$(this).toggle();
return $("#stopbutton").toggle();
});
return $("#stopbutton").on("click", function() {
audioRecorder.stop();
$(this).toggle();
$("#recbutton").toggle();
$("#live").hide();
return $("#entrance").fadeIn("slow");
});
});
</script>
And as you can see, I call the toggleRecording(this) function (the one wich starts the recording process) only after the #recbutton is pressed. Now, everything works fine with this code BUT, the user gets prompted for microphone permission as soon as the page is loaded and I want to ask them for permission to use the microphone ONLY AFTER they clicked the #recbutton Do you understand me? I tought that if I remove the last line of the first file:
window.addEventListener('load', initAudio );
and modify my embedded script like this:
<script type="text/javascript" >
$(function() {
$("#recbutton").on("click", function() {
$("#entrance").hide();
$("#live").fadeIn("slow");
initAudio();
toggleRecording(this);
$(this).toggle();
return $("#stopbutton").toggle();
});
return $("#stopbutton").on("click", function() {
audioRecorder.stop();
$(this).toggle();
$("#recbutton").toggle();
$("#live").hide();
return $("#entrance").fadeIn("slow");
});
});
</script>
I might be able to achieve what I wanted, and actually I am, the user doesn't get prompted for his/her microphone until they click the #recbutton. The problem is, the audio never get's recorded, when you try to download it, the resulting WAV it is empty.
How can I fix this?
My project's code is at: https://github.com/Jmlevick/html-recorder

No, your problem is that getUserMedia() has an asynchronous callback (gotMedia()); you need to have the rest of your code logic in the startbutton call (the toggleRecording bit, in particular) inside that callback, because right now it's getting executed before getUserMedia returns (and sets up the audio nodes).

I found an elegant & easy solution for this (or at least I see it that way):
What I did was toss "main.js" and "recorder.js" inside a getScript call that is executed only when a certain button (#button1) is clicked by the user... These scripts do not get loaded with the webpage itself until the button it's pressed, but we need some more nifty tricks to make it work the way I described and wanted above:
in main.js, I changed:
window.addEventListener('load', initAudio );
for:
window.addEventListener('click', initAudio );
So when the scripts are loaded into the page with getScript the "main.js" file now listens for a click event in the webpage to ask the user for the microphone. Next, I had to create a hidden button (#button2) on the page wich is fakely clicked by jQuery exactly right after the scripts are loaded on the page, so it triggers the "ask for microphone permisson" event and then, just below that line of code wich generates the fake click I added:
window.removeEventListener("click", initAudio, false);
so the "workflow" for this trick ends up as follows:
User presses a button wich loads the necesary js files into the page with getScript, it's worth mentioning that now the "main.js" file listens for a click event on the window instead of a load one.
We have a hidden button wich is "fakely clicked" by jQuery just in the moment you click the first one, so it triggers the permisson event for the user.
Once this event is triggered, the click event listener is removed from the window, so it never fires the "ask for permisson" event again when the user clicks anywhere on the page.
And basically that's all folks! :) now when the user goes into the page he/she never get asked for microphone permisson until they click a "Rec" button on the page just as I wanted. With one click of the user we do 3 things in jQuery, but for the user it seems like nothing happened other that the "microphone permisson message" appearing on the screen instantly right after they click the "Rec" Button.

Related

Intercept calls to HTML5 canvas element

I have a WEB application, that renders it's entire User Interface in an HTML5 canvas.
Note that I can't change the current application.
Currently, this application is being tested using Selenium.
This is done by simulating a click event at a given location in the browser window.
After the click has been executed, a sleep of 2 seconds is being performed to ensure that the entire UI is ready before moving to the next step.
Due to all the 'wait' statements, testing the application is very slow.
Therefore, I thought it was an idea to intercept all calls to the HTML5 canvas.
That way I can rely on the triggered events to know if the UI is ready to move to the next step.
Assume that I have the following code in my application that renders the canvas.
var canvas = document.getElementById("canvasElement");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(10, 10, 100, 100);
Is there a way to intercept the 'fillRect' event?
I tought something along the lines:
var canvasProxy = document.getElementById("canvasElement");
canvasProxy.addEventListener("getContext", function(event) {
console.log("Hello");
});
var canvas = document.getElementById("canvasElement");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(10, 10, 100, 100);
Unforuntately this is not working.
I've created a JSFiddle to play with the example.
https://jsfiddle.net/5cknym74/4/
Amy toughts?
I played a bit around with the JS API and it seems that the following might be working:
// SECTION: Store a reference to all the HTML5 'canvas' element methods.
HTMLCanvasElement.prototype._captureStream = HTMLCanvasElement.prototype.captureStream;
HTMLCanvasElement.prototype._getContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype._toDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype._toBlob = HTMLCanvasElement.prototype.toBlob;
HTMLCanvasElement.prototype._transferControlToOffscreen = HTMLCanvasElement.prototype.transferControlToOffscreen;
HTMLCanvasElement.prototype._mozGetAsFile = HTMLCanvasElement.prototype.mozGetAsFile;
// SECTION: Patch the HTML5 'canvas' element methods.
HTMLCanvasElement.prototype.captureStream = function(frameRate) {
console.log('INTERCEPTING: HTMLCanvasElement.prototype.captureStream');
return this._captureStream(frameRate);
}
HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) {
console.log('INTERCEPTING: HTMLCanvasElement.prototype.getContext');
console.log('PROPERTIES:');
console.log(' contextType: ' + contextType);
return this._getContext(contextType, contextAttributes);
}
HTMLCanvasElement.prototype.toDataURL = function(type, encoderOptions) {
console.log('INTERCEPTING: HTMLCanvasElement.prototype.toDataURL');
return this._toDataURL(type, encoderOptions);
}
HTMLCanvasElement.prototype.toBlob = function(callback, mimeType, qualityArgument) {
console.log('INTERCEPTING: HTMLCanvasElement.prototype.toBlob');
return this._toBlob(callback, mimeType, qualityArgument);
}
HTMLCanvasElement.prototype.transferControlToOffscreen = function() {
console.log('INTERCEPTING: HTMLCanvasElement.prototype.transferControlToOffscreen');
return this._transferControlToOffscreen();
}
HTMLCanvasElement.prototype.mozGetAsFile = function(name, type) {
console.log('INTERCEPTING: HTMLCanvasElement.prototype.mozGetAsFile');
return this._mozGetAsFile(name, type);
}
Now that I can intercept the calls, I can find out which calls are responsible that draw a button and react accordingly.

ajax undefined after a thousand iterations

When creating an animation I try to use javascript for additional effects, namely snow being piled up and falling off the edges of the foreground. I figured that the javascript could do the "calculations" on a <canvas> that had the image, then send the "snow" to a php script that would create the png images. This is all placed on my local "server" because that is the only one that can write files.
<html>
<head>
<title>Making Snow</title>
</head>
<body bgcolor="black">
<canvas id="canvas" width="1920px" height="1080px"></canvas>
</body>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script>
var canvas;
var ctx;
var frame=-530;
var simg = new Image()
var dimg = new Image()
onload = function()
{
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
simg.src = "prodB/C/DGB.0530.png"
}
simg.onload = function()
{
var ipo=3;
// Initialize all pixels on the screen/page
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(simg,0,0);
document.title = "Making Snow " + (frame+530) + " / 7000";
snowdraw();
}
dimg.onerror = function()
{
dimg.src = "../imagewriting/snow" + zeroFill(frame+530,4) + ".png";
}
dimg.onload = function()
{
frame++;
if(frame<530)
simg.src = "prodB/C/DGB.0530.png"
else if(frame>4400)
simg.src = "prodB/C/DGB.4400.png"
else
simg.src = "prodB/C/DGB." + zeroFill(frame,4) + ".png"
}
var snowdraw = function()
{
var temp;
var canvasData = "";
// console.log(screen);
// Animation
// Choose a random pixel at the top of the screen
if(frame<7000)
{
for(ny=canvas.height-2; ny>=0; ny--)
{ // Iterate through all the pixels starting from the bottom right corner.
for(nx=canvas.width-2; nx>=0; nx--)
{
canvasData=canvasData+"1";
}
}
$.ajax({
method: "POST",
url: "makesnow.php",
data:{ imgData: canvasData, frame: (frame+530) }
})
.done(function( msg ) {
dimg.src = "../imagewriting/snow" + zeroFill(frame+530,4) + ".png";
});
}
else
{
document.title = "COMPLETE";
}
}
// http://stackoverflow.com/questions/1267283/how-can-i-create-a-zerofilled-value-using-javascript
// by Peter Bailey http://stackoverflow.com/users/8815
function zeroFill( number, width )
{
width -= number.toString().length;
if ( width > 0 )
{
return new Array( width + (/\./.test( number ) ? 2 : 1) ).join( '0' ) + number;
}
return number + ""; // always return a string
}
</script>
</html>
However, on the 1640th frame (or more precisely frame=1110) ajax is suddenly undefined. The image snow1640.png is created, but the browser tells me ajax is not defined and won't advance passed Making Snow 1640 / 7000. Because of the small random nature for each "snow flake" I can't just pick it up from where I left off, as the snow would jump from one frame to the next. Though I did try that at one point and ajax still stopped after that frame.
I first ran it on the local machine running Firefox (http://127.0.0.1/...) then moved onto another machine on the network which is more powerful running Chrome and both died on that same frame. Thought it might be a memory or file limit so I moved the complete frames out of there. Still nothing.
EDIT: Code now snippit of just the problem.
Also, console.log for data and ajax.responseText seem to be generally empty, both during successful "renders" and when it starts iterating ajax is not defined (every other line empty, every other error).
EDIT: New development! Turns out that the error ReferenceError: ajax is not defined anyways gets called between frames (and the 404 when asking dimg if image was created).
EDIT: By typing in console.log($.ajax) after onload and commenting out simg.src I got function ajax().
Error persists, this time I expanded it to reveal the following:
success http://127.0.0.1/ag/makesnowBC.html 197:7
j http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js 2:27131
fireWith http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js 2:27949
x http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js 4:22242
b http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js 4:26298
EDIT: Changed the code to now use Synchronous ajax. This time there are no error messages what so ever, but it still stops at 1640.

Paper.js Background Rasterization Glitches

We are building an image editor of sorts using Paper.js. We have a queue on the side of the Paper.js canvas that allows switching between images. Each time we switch between images, we want to flatten all the annotations (rasterize) onto the image just being edited.
Each time we switch images, this method is called, which rasterizes the current image and annotations to a data URL. (If we revisit this image, a raster from this data URL will be displayed.)
var flattenToDataUrl = function() {
layerAsRaster = paper.project.layers[0].rasterize(); // Layer to Paper.js Raster object
layerAsRaster.visible = false; // Attempt to set not visible
var dataString = layerAsRaster.toDataURL();
return dataString;
};
Then we end up calling this method, which changes out the image we're editing:
var setCanvasImage = function(imageObject) {
if(imageObject != null)
{
imageHeight = imageObject.height;
var imageWidth = imageObject.width;
// Set up HTMLImage
var imageElement = new Image(imageObject.width, imageObject.height);
if(_.has(imageObject, 'imageData')) { // Came as 64 bit data
imageElement.src = 'data:image/png;base64,' + imageObject.imageData;
} else if(_.has(imageObject, 'imageUrl')) { // Came as URL
imageElement.src = imageObject.imageUrl;
}
// Add image to Paper.js canvas
imageElement.onload = function(event) {
// Initialize Paper.js on the canvas
paper.setup(canvas);
raster = new paper.Raster(imageElement, new paper.Point(canvas.width / 2, canvas.height / 2));
setUpNotes();
selectedItems = new paper.Group(); // Since Paper.js has been setup we can now initialize this Group
registerCanvasEvents(); // Panning, zooming, moving selected items, deselecting all selected items
fitToPage();
};
}
};
So, this changes out the image, but when I move my mouse into the canvas after selecting a different image in the queue, it glitches to the image we were just on (with its annotations) until I do something like pan, zoom, etc. Then I see the image I selected and am truly working with.
Removing the flattenToDataUrl() functionality makes the queue work seamlessly. So it seems to me something is fishy there. We are generating a Paper.js Raster object in that method. Rasters seem to automatically add themselves. I attempt to curb this with a call to
layerAsRaster.visible = false;
but to no avail.
What is causing this glitchy behavior and how do I prevent it?
Update
For clarity (hopefully) and completeness, I've decided to post the whole PaperFunctions class we use in conjunction with React, which hosts our <canvas> element. There's a lot of code, and a lot of cleanup to do, especially in registerCanvasEvents(). Bear with this learning beginner. Also it's several hundred lines, so it may be helpful to paste it into your favorite editor. Entry points include setCanvas which is called in componentDidMount of the React class with the <canvas> element, and canvasSetImage which is called from the queue. I agree from bmacnaughton's answer that it's weird to call paper.setup(canvas) every time we load a new image. I'm currently investigating the right solution to this, the right place to put it. setCanvas seems logical but when I drag the image to move it in that setup, it leaves a trail of images in its wake. Anyway, here's PaperFunctions.js:
var JQueryMousewheel = require('jquery-mousewheel')($);
var SimplePanAndZoom = require('./SimplePanAndZoom.js');
var panAndZoom = new SimplePanAndZoom();
var selectedItems;
// We use selection here in two distinct ways.
// An item may be Paper.js selected but not in the selection group.
// This is because we want to show it is selectable.
// A blue bounding box indicates it is selectable.
// A green bounding box indicates it has actually been selected and added to selectedItems.
// Only things in selectedItems are actually operated on.
// So the event handlers in this method basically set up whether or not the item is in selectedItems (and therefore will be operated on for moving, resizing, deleting, etc.).
// That is, the event handlers here are concerned with displaying to the user the status of selection for the item - whether or not it will be operated on when events actually happen on the selectedItems Group.
var registerItemEvents = function(item) {
// Boolean flag for mouseup to know if was drag or click
var dragged;
// For tracking if dragging or clicking is happening
item.on('mousedown', function(e) {
dragged = false;
});
// On click toggle selection
item.on('mouseup', function(event) {
event.stopPropagation(); // Only for item applied to
event.preventDefault();
if(!dragged) {
var justAdded = addIfNotInSelectedItems(item);
if(!justAdded) { // Item was in selection group, remove it
item.remove();
paper.project.activeLayer.addChild(item);
this.selectedColor = paper.project.activeLayer.selectedColor;
//item.selected = false;
}
}
});
// Show as selectable even after has been deselected
item.on('mousemove', function(event) {
this.selected = true;
})
// If not selected, on mouse enter show that it is selectable
item.on('mouseenter', function(event) {
if(!this.selected) {
this.selected = true;
}
});
// If not selected, on mouse leave remove indicator that is selectable
item.on('mouseleave', function(event) {
var isInSelectedItems = selectedItems.getItem(item);
if(this.selected && isInSelectedItems == null) {
this.selected = false;
}
});
// On drag, move item
item.on('mousedrag', function(event) {
dragged = true;
// If user starts dragging automatically select the item
addIfNotInSelectedItems(item);
});
}
var addIfNotInSelectedItems = function(item) {
var isInSelectedItems = selectedItems.getItem(item);
if(isInSelectedItems == null) { // Item not currently in selection group, add it
selectedItems.addChild(item);
item.selectedColor = 'green';
item.selected = true;
return true; // Was added, return true
} else {
return false; // Already in, return false
}
}
var registerCanvasEvents = function() {
if(paper.view != null && canvas != null) {
// Zoom on mousewheel
$(canvas).mousewheel(function(event) {
event.preventDefault();
var mousePosition = new paper.Point(event.offsetX, event.offsetY);
var viewPosition = paper.view.viewToProject(mousePosition);
var returnedValues = panAndZoom.changeZoom(paper.view.zoom, (event.deltaY * -1), paper.view.center, viewPosition, 1.1);
var newZoom = returnedValues[0];
var offset = returnedValues[1];
paper.view.zoom = newZoom;
paper.view.center = paper.view.center.add(offset);
});
// For tracking if dragging or clicking is happening
var dragged;
paper.project.layers[0].on('mousedown', function(e) { // TODO should be layer 0 in long run?
dragged = false;
});
// Pan on mouse drag
/*paper.project.layers[0].on('mousedrag', function(event) { // TODO should be layer 0 in long run?
if(!event.event.ctrlKey && !event.event.altKey && !event.event.shiftKey) { // No keys (that we use) can be pushed
dragged = true; // We're panning, we don't wish to deselect all items as we would do with a click
paper.view.center = panAndZoom.changeCenter(paper.view.center, event.delta.x, event.delta.y, 0.7);
//event.preventDefault();
}
});*/
// Move selected items on mouse drag
selectedItems.on('mousedrag', function(event) {
event.stopPropagation(); // Don't propogate up or it will register as a pan event
event.preventDefault();
dragged = true; // We're panning, we don't wish to deselect all items as we would do with a click
this.translate(new paper.Point(event.delta.x, event.delta.y));
});
// If was a click and not a drag, deselect selected items
paper.project.layers[0].on('mouseup', function(event) {
if(!dragged) {
var removedItems = selectedItems.removeChildren(); // Remove from selection group, which also removes from display
paper.project.activeLayer.addChildren(removedItems); // Return to display
// Reset selection colors for showing selectable
for(var i =0; i < removedItems.length; i++) {
removedItems[i].selectedColor = paper.project.activeLayer.selectedColor;
removedItems[i].selected = false;
}
}
});
// Initial path object, will be reset for new paths after Alt is released
var path = newPath();
var paths = [];
paths.push(path);
// On mousedown add point to start from
paper.project.layers[0].on('mousedown', function(event) {
if(event.event.altKey && !event.event.ctrlKey) { // Alt key to add a path, but disallow attempting to add text at the same time
if(paths[paths.length-1].lastSegment == null) {
//path.add(event.point, event.point);
paths[paths.length-1].add(event.point, event.point);
} else {
//path.add(path.lastSegment.point, path.lastSegment.point);
paths[paths.length-1].add(paths[paths.length-1].lastSegment.point, paths[paths.length-1].lastSegment.point);
}
}
});
// On mousedrag add points to path
paper.project.layers[0].on('mousedrag', function(event) {
if(event.event.altKey && !event.event.ctrlKey) { // Alt key to add a path, but disallow attempting to add text at the same time
if(event.event.shiftKey) { // Use shift key for freeform
//path.add(event.point);
paths[paths.length-1].add(event.point);
} else { // Default of straight line added to path
//path.lastSegment.point = event.point;
paths[paths.length-1].lastSegment.point = event.point;
}
}
}.bind(this));
var tool = new paper.Tool();
var startDragPoint;
// Capture start of drag selection
paper.tool.onMouseDown = function(event) {
if((event.event.ctrlKey && event.event.shiftKey) || (event.event.ctrlKey && event.event.altKey)) {
startDragPoint = new paper.Point(event.point);
}
};
paper.tool.onMouseDrag = function(event) {
// Panning
if(!event.event.ctrlKey && !event.event.altKey && !event.event.shiftKey) { // No keys (that we use) can be pushed
dragged = true; // We're panning, we don't wish to deselect all items as we would do with a click
paper.view.center = panAndZoom.changeCenter(paper.view.center, event.delta.x, event.delta.y, 0.7);
//event.preventDefault();
}
// Show box indicating the area that has been selected
// For moving area and whiting out area
if((event.event.ctrlKey && event.event.shiftKey) || (event.event.ctrlKey && event.event.altKey)) {
dragged = true;
var showSelection = new paper.Path.Rectangle({
from: startDragPoint,
to: event.point,
strokeColor: 'red',
strokeWidth: 1
});
// Stop showing the selected area on drag (new one is created) and up because we're done
showSelection.removeOn({
drag: true,
up: true
});
}
};
// Capture start of drag selection
paper.tool.onMouseUp = function(event) {
if((event.event.ctrlKey && event.event.shiftKey) || (event.event.ctrlKey && event.event.altKey)) {
var endDragPoint = new paper.Point(event.point);
if(event.event.ctrlKey && event.event.shiftKey) { // Whiteout area
whiteoutArea(startDragPoint, endDragPoint);
} else if(event.event.ctrlKey && event.event.altKey) { // Move selected area
selectArea(startDragPoint, endDragPoint);
}
}
};
// Key events
paper.tool.onKeyUp = function(event) {
// Delete selected items on delete key
if(event.key == 'delete') {
selectedItems.removeChildren();
} else if (event.key == 'option') {
registerItemEvents(paths[paths.length-1]);
// Start a new path
paths.push(newPath());
}
}
}
}
// These variables are scoped so that all methods in PaperFunctions can access them
var canvas; // Set by setCanvas
var imageHeight; // Set by setCanvasImage
var raster;
var toolsSetup = false;
var setCanvas = function(canvasElement) {
canvas = canvasElement;
paper.setup(canvas);
};
var setCanvasImage = function(imageObject) {
if(imageObject != null)
{
imageHeight = imageObject.height;
var imageWidth = imageObject.width;
// Set up HTMLImage
var imageElement = new Image(imageObject.width, imageObject.height);
if(_.has(imageObject, 'imageData')) { // Came as 64 bit data
imageElement.src = 'data:image/png;base64,' + imageObject.imageData;
} else if(_.has(imageObject, 'imageUrl')) { // Came as URL
imageElement.src = imageObject.imageUrl;
}
// Add image to Paper.js canvas
imageElement.onload = function(event) {
//canvas.height = $(document).height()-3; // Set canvas height. Why do this here and not in the React component? Because we set the width here too, so we're keeping those together. Perhaps in the future this will be changed when we are responsive to window resizing.
//scalingFactor = canvas.height / imageObject.height; // Determine the ratio
//canvas.width = imageElement.width * scalingFactor; // Scale width based on height; canvas height has been set to the height of the document
// Initialize Paper.js on the canvas
paper.setup(canvas);
raster = new paper.Raster(imageElement, new paper.Point(canvas.width / 2, canvas.height / 2));
//setUpLineAndFreeFormDrawing(); // TODO once we cycle through images will we need to reset this for each new image or can we do this just once?
setUpNotes(); // TODO once we cycle through images will we need to reset this for each new image or can we do this just once?
selectedItems = new paper.Group(); // Since Paper.js has been setup we can now initialize this Group
registerCanvasEvents(); // Panning, zooming, moving selected items, deselecting all selected items
fitToPage();
};
}
};
var fitToPage = function() {
if(paper.view != null && canvas != null) {
// Fit image to page so whole thing is displayed
var scalingFactor = canvas.height / imageHeight; // Constant representation of the ratio of the canvas size to the image size
var zoomFactor = scalingFactor / paper.view.zoom; // Dynamic representation of the zoom needed to return to viewing the whole image in the canvas
// Reset the center point to the center of the canvas
var canvasCenter = new paper.Point(canvas.width/2, canvas.height/2);
paper.view.center = canvasCenter;
// Zoom to fit the whole image in the canvas
var returnedValues = panAndZoom.changeZoom(paper.view.zoom, -1, canvasCenter, canvasCenter, zoomFactor); // Always pass -1 as the delta, not entirely sure why
var newZoom = returnedValues[0];
var offset = returnedValues[1];
paper.view.zoom = newZoom;
paper.view.center = paper.view.center.add(offset);
}
};
var addImage = function(imageDataUrl) {
if(paper.view != null) {
var img = document.createElement("img");
img.src = imageDataUrl;
var presentMomentForId = new Date().getTime() + "-image"; // For purposes of having unique IDs
img.id = presentMomentForId;
img.hidden = true;
document.body.appendChild(img);
var raster = new paper.Raster(presentMomentForId);
registerItemEvents(raster);
}
};
var setUpLineAndFreeFormDrawing = function() {
if(paper.project != null) {
// Initial path object, will be reset for new paths after Alt is released
var path = newPath();
var paths = [];
paths.push(path);
// On mousedown add point to start from
paper.project.layers[0].on('mousedown', function(event) {
if(event.event.altKey && !event.event.ctrlKey) { // Alt key to add a path, but disallow attempting to add text at the same time
if(paths[paths.length-1].lastSegment == null) {
//path.add(event.point, event.point);
paths[paths.length-1].add(event.point, event.point);
} else {
//path.add(path.lastSegment.point, path.lastSegment.point);
paths[paths.length-1].add(paths[paths.length-1].lastSegment.point, paths[paths.length-1].lastSegment.point);
}
}
});
// On mousedrag add points to path
paper.project.layers[0].on('mousedrag', function(event) {
if(event.event.altKey && !event.event.ctrlKey) { // Alt key to add a path, but disallow attempting to add text at the same time
if(event.event.shiftKey) { // Use shift key for freeform
//path.add(event.point);
paths[paths.length-1].add(event.point);
} else { // Default of straight line added to path
//path.lastSegment.point = event.point;
paths[paths.length-1].lastSegment.point = event.point;
}
}
}.bind(this));
// Each time Alt comes up, start a new path
paper.tool.onKeyUp = function(event) {
if(event.key == "option") {
registerItemEvents(paths[paths.length-1]);
// Start a new path
paths.push(newPath());
}
};
}
};
// Establishes default line style
var newPath = function() {
var path = new paper.Path();
path.strokeColor = 'black';
path.strokeWidth = 10;
return path;
};
var note = "";
var setNote = function(newNote) {
note = newNote;
};
var setUpNotes = function() {
if(paper.project != null) {
paper.project.layers[0].on('mousedown', function(event) { // TODO should be layer 0 in long run?
if(event.event.ctrlKey && !event.event.altKey && !event.event.shiftKey) { // Only Ctrl key to add text
// Add text box
var textBox = new paper.PointText(event.point);
textBox.justification = 'left';
textBox.fillColor = 'black';
textBox.fontSize = 60;
textBox.content = note;
registerItemEvents(textBox);
}
});
}
};
var selectArea = function(startDragPoint, endDragPoint) {
var rasterTopLeftCorner = new paper.Point(raster.bounds.topLeft);
var adjustedStartDragPoint = new paper.Point(startDragPoint.x - rasterTopLeftCorner.x, startDragPoint.y - rasterTopLeftCorner.y);
var adjustedEndDragPoint = new paper.Point(endDragPoint.x - rasterTopLeftCorner.x, endDragPoint.y - rasterTopLeftCorner.y);
var boundingRectangleRasterCoordinates = new paper.Rectangle(adjustedStartDragPoint, adjustedEndDragPoint);
var boundingRectangleCanvasCoordinates = new paper.Rectangle(startDragPoint, endDragPoint);
var selectedArea = raster.getSubRaster(boundingRectangleRasterCoordinates);
var whitedOutSelection = new paper.Shape.Rectangle(boundingRectangleCanvasCoordinates);
whitedOutSelection.fillColor = 'white';
whitedOutSelection.insertAbove(raster); // Whiteout just above the image we're working with
registerItemEvents(selectedArea);
}
var whiteoutArea = function(startDragPoint, endDragPoint) {
var whitedOutSelection = new paper.Shape.Rectangle(startDragPoint, endDragPoint);
whitedOutSelection.fillColor = 'white';
whitedOutSelection.insertAbove(raster); // Whiteout just above the image we're working with
}
var flattenToDataUrl = function() {
layerAsRaster = paper.project.layers[0].rasterize(); // TODO should be layer 0 in long run? // Layer to Paper.js Raster object
layerAsRaster.visible = false;
var dataString = layerAsRaster.toDataURL();
return dataString;
};
module.exports = {
setCanvas: setCanvas,
setCanvasImage: setCanvasImage,
fitToPage: fitToPage,
addImage: addImage,
setNote: setNote,
flattenToDataUrl: flattenToDataUrl
};
Additionally, here's the SimplePanAndZoom.js file for clarity. It uses minimal Paper functions, it mainly just does calculations:
// Based on http://matthiasberth.com/articles/stable-zoom-and-pan-in-paperjs/
var SimplePanAndZoom = (function() {
function SimplePanAndZoom() { }
SimplePanAndZoom.prototype.changeZoom = function(oldZoom, delta, centerPoint, offsetPoint, zoomFactor) {
var newZoom = oldZoom;
if (delta < 0) {
newZoom = oldZoom * zoomFactor;
}
if (delta > 0) {
newZoom = oldZoom / zoomFactor;
}
// Zoom towards offsetPoint, not centerPoint (unless they're the same)
var a = null;
if(!centerPoint.equals(offsetPoint)) {
var scalingFactor = oldZoom / newZoom;
var difference = offsetPoint.subtract(centerPoint);
a = offsetPoint.subtract(difference.multiply(scalingFactor)).subtract(centerPoint);
}
return [newZoom, a];
};
SimplePanAndZoom.prototype.changeCenter = function(oldCenter, deltaX, deltaY, factor) {
var offset;
offset = new paper.Point(-deltaX, -deltaY);
offset = offset.multiply(factor);
return oldCenter.add(offset);
};
return SimplePanAndZoom;
})();
module.exports = SimplePanAndZoom;
Thanks.
I'm taking some guesses here but I'll address some problems in the code that will hopefully address the behavior you're seeing.
First, I presume paper.project.layers[0] is paper.project.activeLayer. Once that has been rasterized 1) the raster is added to the layer and setting visible = false does cause it to disappear when an update is done.
Second, when you invoke paper.setup(canvas) in imageElement.onload you create a new paper project. This project starts out as the active project and makes the previous project "disappear". So when you create a raster with raster = new paper.Raster(...) it goes into the new project, not the old project.
So now there is a hidden (.visible = false) raster in the old project (let's call it project1) and a new version of it in project2.
I'm not sure if this is the intended behavior or not, but when you invoke paper.setup(canvas) for what seems to be the second time then paper seems to notice that they both refer to the same canvas and keeps project1 and project2 in sync. So creating the second project clears the first project's children array. And adding new paper.Raster(...) ends up adding the raster to project1 and project2.
Now I can't tell what the next piece of the puzzle is. You'd need to add some information like 1) where the mouse event handlers are setup and what they are attached to, 2) what setUpNotes() does, 3) what registerCanvasEvents() does, and 4) what fitToPage does.
There are a few globals created, imageHeight and raster that probably aren't intentional. And it's not clear why you need to use new Image() at all - paper.Raster() accepts URLs, including data URLs.
I was surprised paper cleared the first project. It's curious.
Version 2:
Let me take a stab at structuring this using layers. I'd suggest you get rid of multiple projects because having mouse event handlers attached to multiple projects that share the same canvas adds too much complexity.
So, in your code initialization: paper.setup(canvas). Do this once and only once.
Setup the initial image in the single layer initially created by paper.
// this will be inserted into the current layer, project.activeLayer
var raster = new paper.Raster(imageURL, paper.view.bounds.center);
When the image in your queue changes do something like:
// make the existing image/layer invisible
paper.project.activeLayer.visible = false;
// add a new layer which is inserted in the project and activated
var layer = new paper.Layer();
// the new layer is activated, create a raster for the image
var raster = new paper.Raster(imageURL, paper.view.bounds.center);
// now do your normal logic for editing, zooming, etc.
It's really a bit more complicated than that because you have a queue of images and you only want to create a layer the first time you visit an image. You could initialize all the rasters at the outset, something like:
var imageURLs = ["url to image1", "url to image2", "etc"];
imageURLs.forEach(function(url) {
new paper.Layer();
paper.project.activeLayer.visible = false;
new paper.Raster(url, paper.view.bounds.center);
});
// make the first layer visible and activate it
paper.project.layers[0].visible = true;
paper.project.layers[0].activate();
The preceeding code gives you a parallel array to the images in your queue so switching images is straightforward - there is no checking to see if that image has been created or not:
function setImage(index) {
paper.project.activeLayer.visible = false;
paper.project.layers[index].activate();
paper.project.layers[index].visible = true;
}
Finally, I would make sure my mouse handling wasn't causing me problems. From the new code you posted it looks like each project had a global tool that handled 'mousedown', 'mousedrag', and 'mouseup' events, another set of handlers for activeLayer for 'mousedown', 'mousedrag', and 'mouseup' events, and also selectedItems has a handler for 'mousedrag'. I can't keep track of what all the different handlers are supposed to do across projects. I'm guessing that these are the root issue with the flickering you saw.
I would likely just use paper.view.on for 'mousedown', 'mousedrag', and 'mouseup' events. When I get an event I would check to see if anything on the layer was hit by using the following:
paper.project.activeLayer.hitTest(event.point);
Being able to set events on the view is new for paper but very useful. There may be a few other tweaks necessary to handle highlighting unselected items. A relatively straightforward way to handle that is to have a group of selected items and a group of unselected items:
unSelectedGroup.on('mouseenter', function() {
unSelectedGroup.selected = true;
});
unSelectedGroup.on('mouseleave', function() {
unSelectedGroup.selected = false;
});
These should be safe across layers when only one layer is visible at a time. I would set up these group handlers when setting up the images, whether all up front or on an as-needed basis. Alternatively, you could also add paper.view.on('mousemove', ...) and handle the 'mouseenter' and 'mouseleave' events yourself using hitTest as shown above, but either approach should work.
I think using a layer-based approach to your images will keep things in sync. There are enough problems with the project-based approach and many different mouse event handlers that you'll be on more stable ground regardless.

Javascript - how to smoothen out ship movement

Im making a basic game
http://www.jasonhuman.co.za/lazerlazer
The idea is to learn more about javascript and the canvas
The problem im having is that i bound onkeypress on the body element
it only allows for a single keypress to be picked up at a time
and its not all that smooth - the ship lags for a second before starting its movement
how can i make it that the ship immediatly starts moving in the correct direction
as soon as the keypress happens?
here is my code that controls the keypress event:
function keyPress(e){
//w=119, s = 115, a = 97, d = 100
if(e.charCode==119){
player[0].ypos -=15;
playerImg.src = 'images/playerUp.png';
engines.play();
}
if(e.charCode==115){
player[0].ypos +=15;
playerImg.src = 'images/playerDown.png';
engines.play();
}
if(e.charCode==97){
player[0].xpos-=15;
playerImg.src = 'images/playerFW.png';
engines.play();
}
if(e.charCode==100){
player[0].xpos+=15;
playerImg.src = 'images/playerFW.png';
engines.play();
}
//fire a bullet
if(e.charCode==32)
{
//fire a bullet
bullets.push(new init(player[0].xpos+88,player[0].ypos+25));
//gunshots
var gunshot = new Audio('lazer.mp3');
gunshot.play();
}
}
As #Rob Baille says, listen for keydown rather than keypress events.
Your delay is caused because you're reloading the images with every keypress.
Instead preload all the images just once.
Then set your playerImg to the appropriate image as needed in the key handler.
Also, you repeated images/playerFW.png...was that intentional?
Here's an example of your code refactored using an image preloader:
// image loader
var imageURLs=[]; // put the paths to your images here
var imagesOK=0;
var imgs=[];
imageURLs.push("images/playerUp.png");
imageURLs.push("images/playerDown.png");
imageURLs.push("images/playerFW.png");
imageURLs.push("images/playerFW.png");
loadAllImages(start);
function loadAllImages(callback){
for (var i=0; i<imageURLs.length; i++) {
var img = new Image();
imgs.push(img);
img.onload = function(){
imagesOK++;
if (imagesOK>=imageURLs.length ) {
callback();
}
};
img.onerror=function(){alert("image load failed");}
img.crossOrigin="anonymous";
img.src = imageURLs[i];
}
}
function start(){
// All images have been fully pre-loaded
// Allow the game to begin
// Maybe hide the "Play" button until this function is triggered
}
function keyPress(e){
//w=119, s = 115, a = 97, d = 100
if(e.charCode==119){
player[0].ypos -=15;
playerImg = imgs[0];
engines.play();
}
if(e.charCode==115){
player[0].ypos +=15;
playerImg = imgs[1];
engines.play();
}
if(e.charCode==97){
player[0].xpos-=15;
playerImg = imgs[2];
engines.play();
}
if(e.charCode==100){
player[0].xpos+=15;
playerImg = imgs[3];
engines.play();
}
//fire a bullet
if(e.charCode==32)
{
//fire a bullet
bullets.push(new init(player[0].xpos+88,player[0].ypos+25));
//gunshots
var gunshot = new Audio('lazer.mp3');
gunshot.play();
}
}
Well there are two ways I know on how to approach this. You can update your frames more often or you can use a different key down method. To update your frames faster
var sim_interval = 1 / 40; // number of times per second that we step the animation.
This declares as a variable the number of times per second that we step the animation, as stated above. This next bit of code is how to reder the initial state.
render(); // render the initial state.
interval_callback_id = window.setInterval(step, sim_interval); // set the callback and save the identifier.
}
Every reder code will be a bit different your function render(){ might be different it is how to uptdate the system. The other way to do a key down method can be found here How to use spacebar and if statements in JS?

Event Listeners MSPointer are not firing

I am currently trying to learn Javascript and doing the following tutorial (http://www.sitepoint.com/creating-a-simple-windows-8-game-with-javascript-input-and-sound/) however I have run into an issue that I cannot get past.
I have created a canvas element, attached three listeners to the canvas to work with mouse clicks:
canvas.addEventListener("MSPointerUp", endAim, false);
canvas.addEventListener("MSPointerMove", adjustAim, false);
canvas.addEventListener("MSPointerDown", beginAim, false);
But my functions are never being called on PointerUp or Down or Move.
Below are the functions in question, also note that I have done "console.log" just to debug.. None of those are being even recorded to the console, which is why I am thinking that the events are not being triggered..
function beginAim(event){
console.log("Aim ahoy");
if (playerTurn == 1) {
if (!isAiming) {
aimStart = new createjs.Point(event.x, event.y);
isAiming = true;
}
}
}
function adjustAim(event){
console.log("adjustAim event called");
if (isAiming) {
var aimCurrent = new createjs.Point(event.x, event.y);
aimVector = calculateAim(aimStart, aimCurrent);
//ToDo: write text / show aim arror
console.log("Aiming... " + aimVector.x + "/" + aimVector.y);
}
}
function endAim(event){
if (isAiming) {
console.log("endAim Function called");
isAiming = false;
var aimCurrent = new createjs.Point(event.x, event.y);
aimVector = calculateAim(aimStart, aimCurrent);
playerFire = true;
}
}
function calculateAim(start, end){
var aim = new createjs.Point(
(end.x - start.x) / 80,
(end.y - start.y) / 80);
aim.x = Math.min(MAX_SHOT_POWER, aim.x);
aim.x = Math.max(0, aim.x);
aim.y = Math.max(-MAX_SHOT_POWER, aim.y);
aim.y = Math.min(0, aim.y);
return aim;
}
I knew this was going to be a simple issue.. The MSPointerUp /Down / Move are all for Windows8, this is why they never triggered.
I ended up switching to mousedown, mouseup, and mousemove to get the same results.
add to body or canvas to route touch events to JavaScript:
body, canvas {
-ms-user-select: none;
touch-action: none;
}
Then, you'll need to create a MSGesture object, set its target to canvas, also create a pointerdown listener:
var gesture = new MSGesture();
gesture.target = canvas;
canvas.addEventListener("pointerdown", beginAim, false)
in beginAim add a handler for pointerdown and add it to gesture like this:
if (event.type == "pointerdown") {
gesture.addPointer(e.pointerId);
return
}

Categories

Resources