I have a class I am attempting to write that looks like the below.
When I run the class, I get an error:
Audio.js:53 Uncaught ReferenceError: bufferLength is not defined
I believe this is because bufferLength is not available in the drawCanvas function.
I have tried adding the this keyword, however this did not work.
How can I make these variables available to functions within a method of this class?
export const LINE_COLORS = ['rgba(255, 23, 204, 0.5)', 'rgba(130, 23, 255, 0.5)'];
// The dimensions of the current viewport
// - Used to set canvas width & height
export const PAGE_DIMENSIONS = {
width: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth,
height: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight,
};
class AudioEngine {
constructor(params) {
this.params = params;
this.audio = new Audio();
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.analyser = this.ctx.createAnalyser();
this.source = this.ctx.createMediaElementSource(this.audio);
this.dataArray = new Uint8Array(this.analyser.frequencyBinCount);
this.bufferLength = this.analyser.frequencyBinCount;
this.canvas = document.getElementById(params.waveform);
this.onInit();
}
onInit() {
this.ConfigAudio();
this.DrawAudioWave();
}
ConfigAudio = () => {
this.audio.src = this.params.stream;
this.audio.controls = false;
this.audio.autoplay = false;
this.audio.crossOrigin = 'anonymous';
this.analyser.smoothingTimeConstant = 0.6;
this.source.connect(this.ctx.destination);
this.source.connect(this.analyser);
this.analyser.fftSize = 2048;
this.analyser.getByteFrequencyData(this.dataArray);
document.body.appendChild(this.audio);
};
DrawAudioWave = () => {
// Bind to the context
const canvasCtx = this.canvas.getContext('2d');
function drawCanvas() {
// We always start the drawing function by clearing the canvas. Otherwise
// we will be drawing over the previous frames, which gets messy real quick
canvasCtx.clearRect(0, 0, PAGE_DIMENSIONS.width, PAGE_DIMENSIONS.height);
requestAnimationFrame(drawCanvas);
const sliceWidth = (PAGE_DIMENSIONS.width * 1.0) / bufferLength;
// Create a new bounding rectangle to act as our backdrop, and set the
// fill color to black.
canvasCtx.fillStyle = '#000';
canvasCtx.fillRect(0, 0, PAGE_DIMENSIONS.width, PAGE_DIMENSIONS.height);
// Loop over our line colors. This allows us to draw two lines with
// different colors.
LINE_COLORS.forEach((color, index) => {
let x = 0;
// Some basic line width/color config
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = color;
// Start drawing the path
canvasCtx.beginPath();
for (let i = 0; i < bufferLength; i++) {
// We offset using the loops index (stops both colored lines
// from overlapping one another)
const v = dataArray[i] / 120 + index / 20;
// Set the Y point to be half of the screen size (the middle)
const y = (v * PAGE_DIMENSIONS.height) / 2;
if (i === 0) {
canvasCtx.moveTo(x, y);
} else {
canvasCtx.lineTo(x, y);
}
x += sliceWidth;
}
canvasCtx.lineTo(canvas.width, canvas.height / 2);
canvasCtx.stroke();
});
}
drawCanvas();
};
audioToggle = () => (this.audio.paused ? this.audio.play() : this.audio.pause());
}
export default AudioEngine;
requestAnimationFrame will call drawCanvas with the global context bound, so this.bufferLength will not be defined. Easiest solution is to take advantage of lexical scoping, which arrow functions make easy.
If you look at how you're defining your class's methods, they're using arrow functions. This is precisely to avoid the issue with rebinding this that you're currently having. You should read up on this and scoping in JavaScript to better understand why your code isn't working as you'd expect.
Couple solutions, both require drawCanvas -> this.drawCanvas:
(A) Bind the context of drawCanvas before passing it to requestAnimationFrame:
requestAnimationFrame(drawCanvas.bind(this))
Or (B) take advantange of lexical scoping:
requestAnimationFrame(() => drawCanvas())
"Lexical" scope is scope derived from the "text", i.e. where your arrow function is defined relative to other scopes. Non-lexical scoping uses the caller function to determine bound context.
You can also change the drawCanvas function itself to be bound to the appropriate scope using .bind or changing it to an arrow function.
Further Reading:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this
How does the "this" keyword work?
Pass correct "this" context to setTimeout callback? (replace setTimeout with requestAnimationFrame in their examples)
Related
I have this class object that needs to have its draw function inside the constructor to update the score, if it is outside the score returns undefined.
export class Hud {
constructor(world) {
var self = this;
this.canvas = world.canvas;
this.ctx = world.ctx
this.score = 0;
this.draw = function() {
this.ctx.font = "16px Arial";
this.ctx.fillStyle = "#0095DD";
this.ctx.fillText("Score: " + self.score, 8, 20);
}
}
}
my other class objects have the draw function outside of the constructor like so works fine,
export class Ball {
constructor(world) {
var self = this;
this.canvas = world.canvas;
this.ctx = world.ctx;
self.x = canvas.width / 2;
self.y = canvas.height - 30;
this.ballRadius = 10
this.dx = 2;
this.dy = -2
}
draw() {
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.ballRadius, 0, Math.PI * 2);
this.ctx.fillStyle = "#0095DD";
this.ctx.fill();
this.ctx.closePath();
}
}
My question is what the difference between the two are. I thought if its defined in the constructor the variables are accessible throughout the class object. I guess I'm confused, should there be functions outside of constructor function? Having everything in the constructor seems to be hassle free.
Function defined in the class (not in the constructor) live on the class prototype which each instance is linked to. Functions defined in the constructor become own properties of each instance. If you defined the function in the constructor, each instance will get its own copy of the function. If you don't the instances will defer to the prototype chain and all the instances will point to the same function. For example:
class Hud {
constructor(world) {
this.name = world
this.draw = function() {
console.log("draw on instance from", this.name)
}
}
draw_outside(){
console.log("draw on class from", this.name )
}
}
let h1 = new Hud('h1')
let h2 = new Hud('h2')
console.log(h1.__proto__.draw) // draw not on prototype
console.log(h1.__proto__.draw_outside) // draw_outside is
console.log(h1.draw === h2.draw) // each object gets its own draw
console.log(h1.draw_outside === h2.draw_outside) // but both point to the same draw_outside
console.log(Object.getOwnPropertyNames(h1)) // only draw & name
// both access `this` the same way when called on an instance:
h1.draw()
h1.draw_outside()
I just started using Canvas for my web game project and faced a problem.
I'm using this code to render the game:
function render(f){
if(charoffset.x == null) charoffset.x = charpos.x*tilescale;
if(charoffset.y == null) charoffset.y = charpos.y*tilescale;
if(!tiles) tiles = [];
if(f){
log("Welcome.","gold");
}
var canPassthrough = function (){
if ((def.passable(this.type))&&(typeof this.type !== 'undefined')){
return true;
}
else {
return false;
}
};
if(!f) lighting.update();
canvas.getContext("2d").clearRect(0,0,sq,sq);
for (var i = 0; i < map[charlvl].length; i++){
if(!tiles[i]) tiles[i] = [];
for (var j = 0; j < map[charlvl][i].length; j++){
if(!tiles[i][j]) tiles[i][j] = placetile(i,j);
drawtile(tiles[i][j]);
placeitem(i,j);
}
}
ui.overlay.text("casting shadows...");
//shadowcaster(20);
var tex = document.createElement("img");
tex.src = "../img/charplaceholder.png";
var hero = canvas.getContext("2d");
hero.globalAlpha = 1.0;
if(charoffset.x>=map_scroll.x&&charoffset.y*tilescale>=map_scroll.y){
var pos = {
x: charoffset.x - map_scroll.x - tilescale,
y: charoffset.y - map_scroll.y - tilescale
};
hero.drawImage(tex,pos.x,pos.y,tilescale,tilescale);
}
function placetile(x,y){
var obj = {};
obj.type = map[charlvl][x][y].id;
obj.canPassthrough = canPassthrough;
obj.state = {explored: false, lit: false};
obj.coords = {x:x,y:y};
obj.offset = {x:x*tilescale,y:y*tilescale};
return obj;
}
function drawtile(t){
if(t.offset.x>=map_scroll.x&&t.offset.y>=map_scroll.y){
var pos = {
x: t.offset.x - map_scroll.x - tilescale,
y: t.offset.y - map_scroll.y - tilescale
};
if(!t.state.explored&&!t.state.lit){
return false;
}
else if(t.state.lit&&t.state.explored){
var tex = document.createElement("img");
var tile = canvas.getContext("2d");
tex.src = def.css.tile(t.type);
tile.globalAlpha = 1.0;
tile.drawImage(tex,pos.x,pos.y,tilescale,tilescale);
return true;
}
else if(t.state.explored&&!t.state.lit){
var tex = document.createElement("img");
var tile = canvas.getContext("2d");
tex.src = def.css.tile(t.type);
tile.globalAlpha = 0.25;
tile.drawImage(tex,pos.x,pos.y,tilescale,tilescale);
return true;
}
}
}
function placeitem(x,y){
return;
if (loot[charlvl][x][y]){
for(var i=0;i<loot[charlvl][x][y].length;i++){
var tile = document.createElement("div");
var tileid = loot[charlvl][x][y][i].type;
tile.className = def.css.item(tileid);
tile.coords = {x:x,y:y};
document.getElementById("x" + x + "y" + y).appendChild(tile);
}
}
}
if(f){
camera.center(charpos.x,charpos.y);
ui.overlay.text("loading the dungeon...");
ui.overlay.hide();
}
}
Function render() is fired by various events, such as character moving, map dragging, lighting update, etc.
This is the result:
I would like to add inset shadows to walls so it's more clearly visible those are walls. I tried experimenting with canvas context shadows, and used this:
It's supposed to draw a transparent rectangle and a shadow for it at 100, 100 with size 20, 20, however this applies shadow to every drawn tile instead.
I feel like I'm using drawing wrong. Can anyone explain how to effectively
use canvas to achieve desired effect?
Do not use the 2D API shadow options , they are very very slow ( and that is an understatement of how bad they are). You are much better off creating the shadows as part of the tile set and rendering them with either ctx.globalAlpha set to less than 1 and/or use one of the many composite modes. Eg ctx.globalCompositeOperation = "multiply"; Or overlay, color-burn, hard-light, and soft-light. You can even use a combination to get a very good shadow effect.
Creating the shadows as part of the tile set will give a much more realistic effect as the shadow API is just for shadows cast from flat object floating above a flat surface, not for 3D objects protruding from the screen that may have sloped sides in the z direction.
If you do not wish to create the shadows as part of the tile set consider creating the shadow tile set at onload using an off screen canvas via the shadow API options. Then render from that to the canvas using alpha and composite options
cancelAnimationFrame() does not seem to work when called inside an object's method. I have tried binding the this value to the callback function (as demonstrated on MDN with setTimeout) but I received a TypeError when using cancelAnimationFrame(). I then tried setting the this value to a local variable called _this and called cancelAnimationFrame() again. That time, I did not receive an error but the animation itself is still playing. How do I cancel the animation?
I have recreated the issue I am having below. If you open a console window, you will see that the animation is still running.
function WhyWontItCancel() {
this.canvas = document.createElement("canvas");
this.canvas.width = 200;
this.canvas.height = 10;
document.body.appendChild(this.canvas);
this.draw = this.canvas.getContext("2d");
this.draw.fillStyle = "#f00";
this.position = 0;
};
WhyWontItCancel.prototype.play = function() {
if (this.position <= 190) {
this.draw.clearRect(0, 0, 400, 10);
this.draw.fillRect(this.position, 0, 10, 10);
this.position += 2;
} else {
//window.cancelAnimationFrame(this.animation.bind(this));
var _this = this;
window.cancelAnimationFrame(_this.animation);
console.log("still running");
}
this.animation = window.requestAnimationFrame(this.play.bind(this));
};
var animation = new WhyWontItCancel();
animation.play();
Seems that you miss two things here. First, this.animation = window.requestAnimationFrame(this.play.bind(this)); line is invoked always when play() is called. Contrary to what you might think, cancelAnimationFrame only removes the previously requested RAF call. Strictly speaking, it's not even necessary here. Second, you don't have to bind on each RAF call; you might do it just once:
function AnimatedCanvas() {
this.canvas = document.createElement("canvas");
this.canvas.width = 200;
this.canvas.height = 10;
document.body.appendChild(this.canvas);
this.draw = this.canvas.getContext("2d");
this.draw.fillStyle = "#f00";
this.position = 0;
this.play = this.play.bind(this); // takes `play` from prototype object
};
AnimatedCanvas.prototype.play = function() {
if (this.position <= 190) {
this.draw.clearRect(0, 0, 400, 10);
this.draw.fillRect(this.position, 0, 10, 10);
this.position += 2;
this.animationId = window.requestAnimationFrame(this.play);
}
};
You might want to add cancel into your prototype to be able to stop your animation, for example:
AnimatedCanvas.prototype.cancel = function() {
if (this.animationId) {
window.cancelAnimationFrame(this.animationId);
}
};
... but the point is, it's not useful in the use case described in the question.
as the title said i'm trying to animate on a java canvas but i'm not sure how to add on to the vector class of my objects current position
I've been told to use this:
Bus.prototype.update = function()
{
this.getPosition().add(new Vector(10.0));
}
but it doesn't recognize the .add function and comes up with an error when i use my setInterval function
I'll include my get/set functions aswell just incase
Bus.prototype.getPosition = function() {
return this.mPosition;
};
Bus.prototype.setPosition = function (pPosition) {
this.mPosition = pPosition;
};
I'm pretty new at coding so i apologize if this is very vague or badly written
jsFiddle : https://jsfiddle.net/CanvasCode/41z9o10p/1/
javascript
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var Bear = function(xSet, ySet)
{
this.XPos = xSet;
this.YPos = ySet;
}
Bear.prototype.updatePosition = function(xValue, yValue)
{
this.XPos += xValue;
this.YPos += yValue;
}
var bear = new Bear(0,0);
setInterval( function()
{
ctx.fillStyle = "#000";
ctx.fillRect(0,0,c.width,c.height);
ctx.fillStyle = "#0F0";
ctx.fillRect(bear.XPos, bear.YPos, 50,50);
bear.updatePosition(0.2, 0.4);
} ,1);
Bear is a custom "class". All bear has is a X position and a Y position, the constructor can take in two values which will set the X and Y value. I then add a new method to Bear which is called updatePosition. All updatePosition does it take in two values and add them to the original X position and Y position (you can also provide negative numbers to move the opposite way). Now with our Bear we can create one and then use its updatePosition function and move it.
I have this canvas mousedown event:
$('#canvas').mousedown(function(e) {
var coords = canvas.getMousePos(e); // user helper function
if (coords.x >= 0 && coords.x <= 16 && coords.y >= 32 && coords.y <= 48) {
tool = "fill";
} else if (coords.x >= 16 && coords.x <= 32 && coords.y >= 32 && coords.y <= 48) {
tool = "circle";
}
});
I am also drawing images on this canvas like so in my document ready function:
var imgCircle = new Image();
imgCircle.src = "circle.png";
imgCircle.onload = function() {
toolboxContext.drawImage(imgCircle, 16, 32);
}
I don't like using hardcoded values like that in my if statements. I was wondering if there was a way to either get the local coordinates of the images in the canvas or actually register events for the clicking of said images. Thanks for reading.
You can't register directly events on your objects but it's easy to register events on the canvas as in
canvas.addEventListener("mousedown", function(e) {
(you're maybe more interested in "mouseup")
And then you can get the coordinates in the canvas with
e.pageX-$(canvas).offset().left
and
e.pageY-$(canvas).offset().top
(here using jQuery for easy cross-platform positionning of the canvas but you can use standard javascript)
After that, you just have to compare with the positions of your images that you must keep somewhere. I personnally use for that effect a Rect class :
function Rect(x,y,w,h){
this.x = x;
this.y = y;
this.w = w;
this.h = h;
}
Rect.prototype.contains = function(x, y) {
return x>=this.x && x<=this.x+this.w && y>=this.y && y<=this.y+this.h;
};
I think you're expecting this to work like the DOM API. Canvas context objects are just a bundle of methods for painting stuff on a grid of pixels. The grid of pixels is the only real state information that's saved. There's no square or rectangle object holding coords for you. Just methods for splatting rectangles and circles on the map. Maintaining and responding to changes in that info is either DIY or you'll want to look into a library that does it for you.
You CAN however write and listen for new events on generic objects on the fly with jQuery.
var someObj = {};
//bind - unfortunate choice for the generic listening method
$(someObj).bind('HiSomeObj',function(){ alert('hi, whoever triggered this event on me'); });
$(someObj).trigger('HiSomeObj'); //'hi, whoever triggered this event on me'
//note how in the following trigger below how I add properties to the event object
Here's a crude DIY approach taking advantage of that.
//your constructor object - assumes 16px diameter (mostly reusing your code)
function imgCircleObjConstructor16px(xArg, yArg){
var imgCircle = new Image(),
//following 2 are var defs and public for convenience
diameter = this.diameter = 16, //hard-coded for now
coords = this.coords = [xArg,yArg],
that = this; //locks down reference to instance for use in JQ
imgCircle.src = "circle.png";
this.draw = function(x,y){
//some function that clears canvas or resets context for redraws
clearCanvasOrContext();
toolboxContext.drawImage(imgCircle, x, y);
this.coords = [x,y];
}
$('#canvas').mousedown(function(e) {
var mouseXY = canvas.getMousePos(e);
if( mouseXY.x >= coords[0]) && mouseXY.x <= coords[0]+diameter &&
mouseXY.y >= coords[1] && mouseXY.y <= coords[1] ){
//trigger custom events with JQ 'trigger' method
//can't use 'this' because it refers to the jq object for #canvas here
$(that).trigger('circleBoundingBoxClicked', { someProperty:'optional' } );
//note: second arg in trigger func serves no purpose
//just provided as an example of adding properties to event objects
}
}
//might as well tell the circle to draw itself when the object is instantiated:
this.draw();
}
var myCircle = new imgCircleObjConstructor16px(16,32);
//listen to custom events with 'bind' method
$(myCircle).bind('circleBoundingBoxClicked', function(e){
var circleObj = e.target; //JQ puts object args from the $ func under target property
alert('Circle Bounding box at x: '+circleObj.coords[0]+', y:'+circleObj.coords[1]
+' was clicked. someProperty value: ' + e.someProperty);
//After you fix whatever syntax goofs I didn't test for, this should alert when clicked:
//'Circle Bounding box at x: 15, y:32 was clicked. someProperty value: optional'
} );
Now when you use the myCircle.draw method to redraw the circle, the event listener should respond to the new coords.
there is a way to get the coordinate x and y of where the user has touched:
<canvas id="none"></canvas>
<script>
var canvas = document.getElementById("none");
var ctx = canvas.getContext("2d");
canvas.onclick = (e) => {
let x = e.offsetX;
let y = e.offsetY;
console.log(`x: ${x}, y: ${y}`);
}
</script>
with offset