wtf Posted December 29, 2018 Share Posted December 29, 2018 this is my exact code, a single html file. same folder has snake0 to 17 .png (animation). I want 18 canvases (I guess each needs a context, so 18 of those too). mainly so I can rotate them if facing down/up/left/right (you cant rotate an Image() apparently but can rotate a canvas. My problem is that I don't see any png on the screen with the 'array version' of the code. If u comment out the opposite stuff (non array version) I do see the png. It must be some stupid simple problem I'm unaware of, how hard can it possibly be to post a stupid png in an array on the screen? Well, it is javascript so probably in some obfuscated, tricky, counterintuitive way it might be possible, I guess. I mean, I could get a permanent marker and write this code on my bathroom wall. I'd call that bathroomscript, and right now bathroomscript and javascript have equal capability in producing a png on my screen with an array. Should I switch to bathroomscript? At least with that I can flush the toilet and use the sink handles, maybe those have some effect on getting results. <!DOCTYPE html> <title>png problem</title> <meta charset="utf-8"/> <canvas id="canvas" width="700" height="550" oncontextmenu="return false"></canvas> <script> function main(){ if (paused==false){ ctx.fillStyle='rgb(128,128,128)' ctx.fillRect(0,0,canvas.width,canvas.height) ctx2.fillStyle='rgb(255,255,255)' ctx2.fillRect(0,0,canvas2.width,canvas2.height) ctx2.drawImage(snakecans[1],0,0) //does not work with array (comment out the opposite line and block below when testing) //ctx2.drawImage(snakecan,0,0) //does work (no array) ctx.drawImage(canvas2,0,0,canvas2.width,canvas2.height, 5,5,canvas2.width,canvas2.height) } } //does not work (comment out opposite block and line above when testing) //can't see any png on screen //i have 18 pngs. i want to put them in 18 'canvases' (so i guess i need 18 contexts too) snakecans=[] snakectxs=[] for(i=0;i<18;i++){ snakecans.push(document.createElement('canvas')) snakecans[i].width=60 snakecans[i].height=30 snakectxs.push(snakecans[i].getContext('2d')) im=new Image() im.src='snake'+String(i)+'.png' im.onload=function(snakectxs,i){ //you apparently need this onload or the png wont load in time snakectxs[i].drawImage(im,0,0) }(snakectxs,i) //says 'snakectxs[i] is undefined' unless I put that stuff in parentheses } //does work //can see png on screen /* snakecan=document.createElement('canvas') snakecan.width=60 snakecan.height=30 snakectx=snakecan.getContext('2d') im=new Image() im.src='snake'+String(3)+'.png' im.onload=function(){ snakectx.drawImage(im,0,0) } */ canvas=document.getElementById('canvas') ctx=canvas.getContext('2d') canvas2=document.createElement('canvas') canvas2.width=540 canvas2.height=540 ctx2=canvas2.getContext('2d') paused=false setInterval(main, 1000/60) </script> Quote Link to comment Share on other sites More sharing options...
mattstyles Posted January 2, 2019 Share Posted January 2, 2019 Hi @wtf, welcome to the forums. First up, blaming tooling when you don't know how to operate it is a good way to always be frustrated by your tools. You're making some errors in using JS, these are all fundamental rules to using the language though, and not quirks (of which it, and browsers, have plenty, but you're not running foul of them here). for (i = 0; i < 18; i++) { im = new Image() im.src = 'foobarbaz' im.onload = function (snakectxs, i) { // stuff }(snakectxs, i) } This does not do what you think it does, for at least two reasons. Immediately Invoked Function Expressions (link) In JS we invoke functions with the `()` syntax. You can do this whenever you want so long as you have a reference to the function declaration and it is currently in scope. This is pretty standard for many languages, certainly not quirky behaviour. You have no reference to the function, but it doesn't matter, you have immediately invoked it anyway. What this has done is invoked the function and applied its return to `im.onload`. The return of that function is null. There are at least 3 issues here: * You're not attaching a function as a listener. * The function is immediately invoked, rather than waiting for the image to load, hence, you're drawing without an image to draw. * You have left `im` as global, so it just gets over-written each time, there are 18 of them sure, but only one reference, which will be to the last one. Not that it matters here anyway. Asynchronicity Images load over the network, that takes time. JS interacts with most browser APIs in an asynchronous manner and it does this using event emitters i.e. the browser or DOM elements emit events that JS listens for and responds to. So... you're on the right track trying to create a listener, its just that you're invoking the function and not actually creating a listener. Again, most C-like languages work this way, JS is no different here. A solution? There are many many ways to achieve what you are after, some simple and some involving some interesting concepts made possible by the flexibility of JS, for example: for (var i = 0; i < 18; i++) { var im = new Image() im.src = 'foobarbaz' im.onload = function (i) { return function () { console.log(i) } }(i) } This way returns a function, thus, invoking the exterior function immediately returns the interior one which gets applied as an `onload` listener. `i` is then scoped to the closure (in this case the function scope) and is as you would expect. I wouldn't suggest you need to do the above, it merely highlights the power and flexibility of JS by treating functions as first class citizens. Some other languages also have this power but it is a common, if slightly advanced, technique in JS (probably more so than other languages). To return to your initial solution design, you don't need 18 canvases just to rotate images. You can do this by applying a translation, drawing, and then unapplying. Again, this is fairly standard across languages and platforms for using raw drawing ops, ordinarily you'd use higher abstraction libraries to help, but you can get your hands dirty if you like, nothing wrong with that. This is a dirty example showing one rotated rectangle and one not: <script> var c = document.getElementById("myCanvas"); var ctx = c.getContext("2d"); ctx.rotate(20 * Math.PI / 180); ctx.fillRect(50, 20, 100, 50); ctx.rotate(-20 * Math.PI / 180) ctx.fillRect(10, 10, 40, 20) </script> It is years since I worked that low-level with Canvas context, but reading the docs is really very helpful. Good luck in your project and with learning JS. hotfeet 1 Quote Link to comment Share on other sites More sharing options...
wtf Posted January 3, 2019 Author Share Posted January 3, 2019 Thanks for taking the time to reply. I like being overly dramatic for entertainment. In the 2 day downtime of getting this post moderator-approved I found a solution at https://stackoverflow.com/questions/17578280/how-to-pass-parameters-into-image-load-event. Out of all of those answers, only the "private scope closure" worked, so here is the code that works (which looks like your middle solution but this has extra parenthesis around the function: snakecans=[] snakectxs=[] snakeimgs=[] for(i=0;i<18;i++){ snakecans.push(document.createElement('canvas')) snakecans[i].width=60 snakecans[i].height=30 snakectxs.push(snakecans[i].getContext('2d')) snakeimgs.push(new Image()) snakeimgs[i].src='snake'+String(i)+'.png' snakeimgs[i].onload=(function(i){ return function(){ snakectxs[i].drawImage(snakeimgs[i],0,0) } })(i) } To me that's really ugly and non-obvious (instead of intuitively building this from scratch next time I need it, I'll be like, ok, how do you build that weird double function monstrosity again? and have to look at my previous code to remember. I'll probably just conclude "you do this weird thing" to load images without fully understanding and focus on normal linear code as much as possible. I understand javascript can't have a single "while(1)" loop, instead Main is a function called rapidly, so other stuff has a chance to run between function calls I guess. In python/pygame, you would just go "for event in all events in the queue, do whatever for that event" at the beginning of each iteration of the main loop. at least that organizes asynchronous code (which kinda reminds me of "goto considered harmful") meaning code based on events that occur after an unknown event delay at the top of each main game loop. Maybe I'm not thinking clearly but it seems one logical way would be to go "startMainFunction=false... while(1)- if image loaded is in event queue, startMainFunction=true". then start the main loop after images loaded. That seems more linear/ orderly. immediately invoked function expression... I don't see the point. it's a function that has "do A, do B, do C" inside and executes immediately... so why not just have "do A, do B, do C" without a function wrapping it? seems identical. Here was another wrong attempt. I guess the problem is "onload" requires a function, but one that needs i as a parameter. but when you do loadfunc(i) instead of loadfunc, it executes the function instead of assigns it. So I guess the only way to assign a function with a parameter to onload is that bizarre double function in the first code snippet. u could do below with a global i instead of a parameter but then I think it might be 18 by the time loadfunc executes. Basically I easily understand the simplicity of python/pygame event handling, or at least enough to only need to think about it peripherally, but this whole onload thing looks like a weird mess to me. function loadfunc(i){ console.log('d') console.log(i) snakectxs[i].drawImage(snakeimgs[i],0,0) } //works snakecans=[] snakectxs=[] snakeimgs=[] console.log('a') for(i=0;i<18;i++){ can=document.createElement('canvas') can.width=60 can.height=30 ctxx=can.getContext('2d') img=new Image() img.src='snake'+String(i)+'.png' snakecans.push(can) snakectxs.push(ctxx) snakeimgs.push(img) } console.log('b') for(i=0;i<18;i++){ console.log('c') snakeimgs[i].onload=loadfunc(i) //wrong cuz parenthesis run instead of assign function, but u need parameter } console.log('e') by the way 18 canvases/contexts are for animation frames facing right. So 18x4 would cover right left up down. my guess is store all rotations first because it'd be computationally costly to rotate each time (raster png not vector). Quote Link to comment Share on other sites More sharing options...
mattstyles Posted January 4, 2019 Share Posted January 4, 2019 Yeah, the thing is, JS is single-threaded by design so it must have a top-notch event pump, as it is quite comfortably now the most-used language on the planet (probably by orders of magnitude) that event loop is about as tight as it can be (and its actually nothing to do with JS, its basically this, incase you're really interested). Not that you should really care about the event loop with JS, unlike Python, you don't (and really shouldn't need to) work that low-level. Execution context and the 'main' function is handled for you, hence the 'script' is JavaScript. By and large JS has 3 methods for dealing with asynchronicity (which it does *all* of the time): * Callbacks * Promises * Async/await Ignore async/await for now, (although if you like reading linear code it'll be very attractive to you), it is supported in all major browsers but not old ones (you can polyfill, but, lets not go there for now). Promises are kinda new for JS, I say kinda new because NodeJS actually started with promises, then switched to using callbacks as standard async method handling. Promises are officially JS language now and have been for a while (couple of years, so not really new), and support is excellent. Browser APIs are largely callback based (some newer ones, like fetch, support promises, which means they also support async/await). Image loading is a browser API, and it'll expect callbacks. You can convert callback-based calls in to promise-based ones easy enough, but I think that is just going to confuse things even more. Your only confusion is with how JS scopes variables, and, you are far from alone, it is quite easily the single biggest gotcha when getting above beginner level JS appreciation. There are a ton of articles on JS scoping, unfortunately some are just wrong or confusing, sigh, unlike some other languages JS has a huge (probably the largest) community so this is probably to be expected. You're making it more difficult by mixing synchronous (for loops) and async, which you totally have to do from time to time. Browsers apps have this odd requirement that many other apps don't, they are expected to load and display something super quick (sub second ideally, sub 2 seconds usually, even on crappy network) but they are now also expected to be fully fledged and complex applications rather than static pages. It is an incredibly tough environment, I think we all probably disregard actually how difficult web apps are in terms of user expectation. Scope https://developer.mozilla.org/en-US/docs/Glossary/Scope JS uses block-scoping, generally when you create a block (i.e. { }) then variables declared within that block will be available. However, within that block variables declared in ancestor blocks will also be available, hence why globals (on the `window` in the browser) are always available. Variables flow down blocks but not up. Functions are usually assigned to variables too. function a () { var b = 1 function b() { var c = 2 console.log(b, c) // #1 } b() console.log(b, c) // #2 } a() // Will error as `c` is not available at #2 `c` is only available within the block scope defined by fn b(). Note that most compiled languages would catch this at compile time and your code would not run. JS is dynamic so you'd get the log from #1, you'd then hit the error at runtime. This is both good and bad. The good at least is that debugging is way way easier than other languages, the largest bad is that you get runtime errors. There are loads of initiatives to make this better, whether they work or not is largely a matter of opinion. Closure https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures JS devs beyond beginner level must get a good grasp on closures as it is the way we can pass context around (there are other ways, using prototypes and following OOP and Classical programming but, they actually make the mental model more difficult and can expose more edge-cases where bugs like to lurk, it is never-the-less a very popular paradigm in JS). Closures and lexical scope define lifetimes for variables, again, this isn't so different from other c-like languages. Thunk A 'thunk' is a term for a common closure pattern, which is the double function thing we employed earlier as a possible solution. function a (i) { var j = 2 return function b (k) { console.log(i, j, k) } } var fn = a(1) fn(3) // 1 2 3 In this example the variables i, j and k are all available to the interior function b, which we actually return from an invocation and thus can use. In effect, we are priming the `i` variable, trapping the `j` variable and allowing the call-site to supply `k`. This is incredibly powerful. Really very very powerful. It uses closure/scope to 'warm-up' functionality with data. It can even be used to create 'private' variables if that is your thing. 13 hours ago, wtf said: To me that's really ugly and non-obvious (instead of intuitively building this from scratch next time I need it, I'll be like, ok, how do you build that weird double function monstrosity again? and have to look at my previous code to remember. It's actually (although arguably) incredibly elegant. It is certainly a wonderful way to separate concerns by supplying data where it makes sense to generate functions. Try googling for 'currying', 'schoenfinkelisation' or even 'functional programming', it is an incredibly common technique in FP, infact some FP languages restrict you to supplying only one parameter at a time to functions and thus enforce function currying. It generally isn't something OOP-strict languages employ often as they either don't support passing around functions as first class citizens or they prefer using classes or structures that have their own inherent context. Pros and cons to this approach, it actually does not marry very well with JS scoping as JS is *not* a classical language, it is instead prototypal (ignore the newer class syntax, it doesn't use traditional c++ style classes). Still, you'll see this pattern all over JS code. 13 hours ago, wtf said: I'll probably just conclude "you do this weird thing" to load images without fully understanding and focus on normal linear code as much as possible. There are always multiple ways to skin a cat in JS. You could create those DOM Image elements and load all images you need up front. You could put them all in the spritesheet and load only one image and cut it up later. You could inspect what `onload` actually returns and maybe use that (if useful) to determine what to do once the image has loaded. 13 hours ago, wtf said: immediately invoked function expression... I don't see the point. They are just thunks, but immediately invoked rather than passing back a function (currying). They are less powerful as you go only go one level deep. They are used to create closures to hold variables. I haven't written one in years. 13 hours ago, wtf said: u could do below with a global i instead of a parameter but then I think it might be 18 by the time loadfunc executes. Totally correct, hence why one possible solution is to trap `i` inside the closure that you create by using a thunk or IIFE. Sync to Async to Promise Again, there are multiple ways to skin a cat, you could do this by turning all of those `onload` callback-style functions in to promises and synchronously iterating when they have all completed, JS even gives you language constructs to do this kind of thing. It's a little advanced but it would involve turning the DOM-based callback `onload` function in to one that returns a promise, then stuffing all promises in to an array, then pass them to `Promise.all` which only invokes when all promises have resolved and then iterate over the results, which again will be an array and you can do what you like with the images once they have all loaded. It's totally up to you. This way sounds like loads of boilerplate, but, that's where off-the-shelf modules help and its a very very powerful pattern to use if you wanted to (it scales really well too). Sounds like more trouble than its worth in this case though. As mentioned earlier, you can load all these things up front, then this part of the code can actually be synchronous. You'd have to deal with loading images async earlier on though, but maybe it would be an easier mental model, it probably wouldn't even slow things down for the user in this case, you gotta load them at some point. 13 hours ago, wtf said: Basically I easily understand the simplicity of python/pygame event handling, or at least enough to only need to think about it peripherally, but this whole onload thing looks like a weird mess to me. Python, Rust, C, Go, etc, programs created with these languages all run fundamentally differently to how JS executes, particularly in the browser. In short, the `onload` function is an example of publisher/subscriber pattern (or you may have heard of it as observer pattern), it is an event emitter, nothing more. I don't know Python well enough but I'd bet it allows the same patterns, possibly identical as they're pretty standard Computer Science and have been for a long time. Linear and synchronous is really nice, however, neither browsers nor poor ole single-threaded JS encourage it as a solution for every problem. A note on creating variables snakecans=[] snakectxs=[] snakeimgs=[] console.log('a') for(i=0;i<18;i++){ can=document.createElement('canvas') can.width=60 can.height=30 ctxx=can.getContext('2d') img=new Image() img.src='snake'+String(i)+'.png' snakecans.push(can) snakectxs.push(ctxx) snakeimgs.push(img) } Be really really careful with code like this. At no point do you explicitly create the variables `snakecans`, `snakectxs`, `snakeimgs`, `i`, `can`, `ctxx` or `img`. This is usually bad, very bad, it is worth setting up linting rules to prevent it as its so bad. Without using `let`, `var` or `const` to explicitly tell JS you want new variables, JS will assume you are trying to access a global variable, and, when it doesn't find one (first time) it will actually create one there and assign it. Root scope, or global, in browsers is `window`. So, all those variables will be available globally, which also certainly is not what you want, and will bite you at some point (it creates horrible horrible bugs, ones that are super tricky to find and create super weird behaviour). What this means is that when you iterate for a 2nd time throughout the for block, you aren't creating new `can`, `ctxx` and `img` anymore, you're reusing the previously assigned ones, albeit over-writing them. This will bite you. You are also leaking the `i` variable, again, this will bite you at some point. The reason this code works at all is because, in these examples, you never want to use those variables aside from after they are assigned and you just got lucky with the examples that you aren't trying to reuse them in a callback (in our thunk example we effectively create a copy of the variable scoped correctly so we don't get the incorrect global `i`, which, as you noted, would be 17 by the time the callbacks are invoked). In non-trivial code you'll end up trying to use them at some point, and weird things could start happening, things that are tricky to find the cause of. hotfeet 1 Quote Link to comment Share on other sites More sharing options...
wtf Posted January 4, 2019 Author Share Posted January 4, 2019 I appreciate the help and will look into those links/terms. mattstyles 1 Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.