processing / p5.js

p5.js is a client-side JS platform that empowers artists, designers, students, and anyone to learn to code and express themselves creatively on the web. It is based on the core principles of Processing. http://twitter.com/p5xjs —
http://p5js.org/
GNU Lesser General Public License v2.1
21.52k stars 3.3k forks source link

Sketch instantiation proposal #113

Closed evhan55 closed 10 years ago

evhan55 commented 10 years ago

Hi Everyone,

For some background, here is a document that we created during a big hands-on meeting last Friday during discussions: https://docs.google.com/document/d/1CAp4n3YtW6a6KSRosEcGf3T2ph_Id_7WlrkOXyDCShQ/edit

Below, please review a proposal that Lauren and I put together of the API for instantiating sketches in the base cases. Let us know what you think! @bobholt @brysonian @kadamwhite, etc.

// API
preload(): runs once, first
setup(): runs once, second
draw(): loops, indefinitely
createCanvas(w, h): creates a canvas element at the 0,0 with input size

// NEXT QUESTIONS TO ANSWER
// 
// draw() vs. loop()?
// What did we decide about createHTMLElement() and the other DOM calls?
// What are the different mouse accessors?

// CASE 0
// No setup() and draw().
// createCanvas() gets called automatically behind the scenes and creates a default
// canvas at 0,0 with a default size and background color.
fill(255, 0, 0);
ellipse(10, 10, 50, 50);

// CASE 1
// Only setup().
// setup() runs once and createCanvas() gets called automatically with defaults.
function setup() {
  background(255, 0, 0);
  noStroke();
  ellipse(0, 0, 50, 50);
}

// CASE 2
// Only setup() and createCanvas().
// setup() runs once and createCanvas() returns a pointer to the canvas created
// with the input size, at 0,0.  Holding the pointer is optional.
function setup() {
  createCanvas(400, 400); 
  background(255, 0, 0);
  noStroke();
  ellipse(0, 0, 50, 50);
}

// CASE 3
// Only draw().
// createCanvas() is called automatically with defaults.
function draw() {
  ellipse(random(0, 400), random(0, 400), 50, 50);
}

// CASE 4
// setup() and draw() without createCanvas().
// createCanvas() is called automatically with defaults.
function setup() {
  background(255, 0, 0);
}
function draw() {
  ellipse(random(0, 400), random(0, 400), 50, 50);
}

// CASE 5
// setup() and draw() with createCanvas().
function setup() {
  createCanvas(400, 400);
  background(255, 255, 0);
}
function draw() {
  ellipse(random(0, 400), random(0, 400), 50, 50);
}

// CASE 6
// setup() and draw() with createCanvas(), holding pointer
var canvas;
function setup() {
  canvas = createCanvas(400, 400);
  canvas.position(100, 50); // allows you to set position, id, etc
  background(255, 255, 0);
}
function draw() {
  ellipse(random(0, 400), random(0, 400), 50, 50);
}
kadamwhite commented 10 years ago

To clarify case 2, I believe this would be supported as well:

function setup() {
  var cnv = createCanvas(400, 400); 
  cnv.background(255, 0, 0);
  cnv.noStroke();
  cnv.ellipse(0, 0, 50, 50);
}

This is a variant of Case 6.

I recall us being strongly in favor of re-naming draw() to loop(), to make it more accurately describe what it does. If we need to maintain reverse-compatibility, I would err towards making loop() the canonical P5.js drawing loop method, and aliasing draw to loop for back-compat.

evhan55 commented 10 years ago

+1 for case 2 clarification, I think that is still consistent with the proposal, and we left it out to reduce noise. Thanks for clarifying.

My take on loop() vs. draw() is that it will be easier to answer once we have the DOM API clarified, which won't happen until we have this instantiation API clarified.

kadamwhite commented 10 years ago

Additionally, we looked at a few ways to instantiate P5 without the global window context:

var sketch = P5({
  setup: function() {
    this.rect( 0, 0, 100, 100 );
  }
});

and we also proposed a way to gain access to the implicitly-created canvas in CASE 1 by passing it in as an argument to setup:

function setup( canvas ) {
  canvas.rect( 0, 0, 100, 100 );
}
kadamwhite commented 10 years ago

The canvas-as-setup-method-argument case outlined above could serve as an intermediate step between "you're calling everything globally" and "you're working entirely within an instantiated instance and have to know how this works"

lmccart commented 10 years ago

@kadamwhite yes, all great points. @evhan55 can we include these cases in the code above for clarity?

also with the instantiation example, there is the option to pass in an id.

var sketch = P5({
  setup: function() {
    this.background( 0, 100, 100 );
  },
  draw: function() {
    this.rect( 0, 0, 100, 100 );
  },
 id: 'canvas0'
});

I think like that? The idea is you can either:

  1. pass in no id (creates a canvas and appends it to body, not recommended if there's other stuff on the page)
  2. pass in a canvas id (attached p5 instance to that canvas), throws error if canvas is already in use by another p5 instance
  3. pass in any other id (creates canvas and appends it to the element with specified id)
kadamwhite commented 10 years ago

We also discussed the ability to pass in not only an ID, but potentially an actual DOM node (either a canvas or a container) as the first argument for the constructor. I'm ready to write tests to verify whatever behavior we settle on for these constructors, as soon as we get to agreement in this thread.

lmccart commented 10 years ago

cool, I'm into flexibility, but can you explain the use case for this? is it something like if you were using jquery to select an element?

kadamwhite commented 10 years ago

You could get access to the DOM in any number of ways, including jQuery and document.querySelectorAll. The main way I would use this is if I was writing a script to both generate the HTML and also instantiate a sketch. This would let me do things like dynamically generate sketches on a page from scratch based on user input, and clean them up afterwards.

kadamwhite commented 10 years ago

As a correlary: If I was writing an IDE for P5, the IDE would have a canvas or preview pane of some sort. I'd be maintaining a reference to that pane's DOM container already, so it would be expedient to pass that in directly rather than passing in an ID and having to re-select an element for which I already have a reference.

lmccart commented 10 years ago

ah, all makes sense, sounds good!

brysonian commented 10 years ago

This looks great.

As for loop vs draw. I agree that loop is better, in theory. In practice however, if the goal is to have this eventually be a replacement for processing (java) then switching to loop will make thousands of tutorials and sample code just that much more confusing. It'll be hard enough for a newcomer to port something, i don't think we should make it more difficult just for the sake of pedantry.

kadamwhite commented 10 years ago

@brysonian "Replacement" is tricky. Java is an awesome environment, but it brings a lot of its own preconceptions to the table, and those impacted the design of Processing (classes, etc). @lmccart described P5.js as being an attempt to take the principles of Processing and apply them to the web, which by virtue of being a different environment with its own structures and patterns puts it at odds with maintaining a 1:1 API under the hood.

To me, the idea of a draw loop is a principle; having it called draw is a legacy attribute of the original implementation. That's why I was proposing to implement P5.js with the most semantically accurate language possible, and to have a compatibility layer that would shim in the specific methods Processing users expect. I feel like it's stronger to call a loop a loop, and have the loop be run through an abstraction where we can plug in "if draw exists and loop does not, use that for loop".

brysonian commented 10 years ago

@kadamwhite I'm not arguing for a 1:1 implementation of the API, not in the slightest. By replacement, i mean a pedagogical replacement, since education has always been a goal of Processing, and is a goal of this project. The idea is to replace many of our classes that have used processing for years with p5.js.

The initial versions of processing used loop instead of draw, and @benfry and @reas made a decision to use draw, for better or worse (it was quite a heated change); but there are many good reasons to keep it:

FWIW, libcinder has an update() method which is called before draw to try and separate out code that is updating state from code that is actually drawing, perhaps something like that is a good middle ground?

lmccart commented 10 years ago

@brysonian thank you for such a thoughtful breakdown of the thought process and decisions behind draw vs loop. I think the point about "No not that loop the other loop" is a really convincing one. as programmers, it's easy to take for granted some things that seem intuitive to us might not seem intuitive to a beginner.

I also think it could be confusing to have a function called noLoop() that causes loop() to be called once. I would expect in this case that loop() not be called at all.

@tafsiri also made a good point that it's sort of more of a pseudo loop than a loop and could be confusing then to name the function loop.

kadamwhite commented 10 years ago

+1 for avoiding the name conflict with other types of loop, I hadn't considered that angle at all. Thanks very much for the detailed response, I'm more than convinced! :)

tafsiri commented 10 years ago

I wouldn't even say that draw() is a pseudo-loop, it is just a function that gets called in a loop (most of the time); which pedagogically is an important distinction for me (as learners will eventually learn to call their own functions in various loops). I think @brysonian articulated the issues I had with loop() (and the contextual advantages of draw()) pretty well.

evhan55 commented 10 years ago

Thanks @brysonian , makes so much sense.

I am curious what p5.js sketches that don't draw to the canvas (and instead work mainly with DOM elements) will look like.

evhan55 commented 10 years ago

I will be implementing the API outlined above this week and will leave this issue open as a place to come raise questions as I come across them.

evhan55 commented 10 years ago

@lmccart @kadamwhite Is there a consensus on the different ways to instantiate a P5 instance without attaching to the global context? (i.e. within the P5 namespace, to allow for multiple instances, etc).

If so, can someone summarize the different ways to instantiate here with all the different options, etc?

If not, let's try to finalize that! :haircut: (first icon I found, it's a girl getting a haircut)

lmccart commented 10 years ago

From above, incorporating @kadamwhite's option to pass in a DOM element:

var sketch = P5({
  setup: function() {
    this.background( 0, 100, 100 );
  },
  draw: function() {
    this.rect( 0, 0, 100, 100 );
  },
 id: 'canvas0'
});

The idea is you can either:

  1. pass in no id or elt (creates a canvas and appends it to body, not recommended if there's other stuff on the page)
  2. pass in a canvas id or elt (attached p5 instance to that canvas), throws error if canvas is already in use by another p5 instance
  3. pass in any other id or elt (creates canvas and appends it to the element with specified id)

I think the variations of setup + no draw, no setup + draw, etc as outlined in the base cases hold true for this as well? Though you would not have the case where you pass in code (case 0) without setup or draw, right?

Question: what should this optional argument be called? id, elt, target, something else?

kadamwhite commented 10 years ago

If we hold a reference to the element I'd recommend calling that 'el', a widespread convention in backbone and jQuery docs. That doesn't answer the question of what to call the parameter―I have thoughts, will post them when I'm not on my phone.

lmccart commented 10 years ago

also, while we're changing it up, should it be createCanvas() or just canvas()?

REAS commented 10 years ago

createCanvas() is the way to do it consistently with Processing. createShape(), createGraphics(), etc.

tafsiri commented 10 years ago

I'd go for createCanvas(), since it actually makes a new thing. To me canvas() is more ambiguous as to whether it operates on an existing thing or makes a new thing.

evhan55 commented 10 years ago

:+1: on 'it actually makes a new thing'

kadamwhite commented 10 years ago

I'm not a big fan of P5({ id: 'canvas0' });—to me, having this be on the same object as the other methods conflates two different questions.

One question is "do I want this P5 instance to be global, or non-global." We've come to a general consensus that the way to do this is to pass a parameter to the P5 constructor that defines a setup or draw method.

The other question is "do I want to attach this P5 instance to an existing canvas, or create a new one." To me, that's separate from the global/non-global question; I would vote to make the DOM reference a separate (optional) argument.

I'm going to run through all the examples we've been over so far, for discussion: Which of these would we want to have be valid?

// Global / Implicit
// Create a globally-bound P5 instance, with an implicitly-created canvas element
P5();

// Global / Explicit, passing canvas element ID
P5( 'canvas-id' );

// Global / Explicit, passing container ID
// If container has a canvas, that is used; if not, a canvas is created within it
P5( 'div-id' );

// Global / Explicit, passing canvas node
var canvasNode = document.getElementById( 'canvas-id' );
P5( canvasNode );

// Global / Explicit, passing container node
var divNode = document.getElementById( 'div-id' );
P5( divNode );

// Local / Implicit, passing object
P5({
  setup: function( canvas ) {
    canvas.rect( 0, 0, 100, 100 );
    // calls to `this` would map through to the implicit canvas
    this.rect( 0, 0, 100, 100 ); // equivalent to the canvas.rect call
  }
});

// Local / Explicit, passing a node (or an ID, or... etc)
P5( canvasNode, { setup: function() { /* ... */ });

// Local / Explicit, passing a function (à la Processing.js)
P5( canvasNode, function( pInstance ) {
  pInstance.setup = function( canvas ) {
    // I would assume all the following would be equivalent:
    pInstance.rect( 0, 0, 100, 100 );
    this.rect( 0, 0, 100, 100 );
    canvas.rect( 0, 0, 100, 100 );
  };
});

// Local / Implicit, passing a function
P5(function( pInstance ) {
  pInstance.draw = function() { /* ... */ };
});

// Define an instance, do other stuff later?
var instance = P5({});
instance.draw = function() { /* ... */ };

I would 100% support the pass-a-context-object variant, as well as (naturally) the global variants. I'd also like to make a case for either supporting the Processing.js format, or else making the instantiation method pluggable so that I can write in support for it... I spent some time the other week playing with the object and function models in my head, and came to the conclusion that having the processing instance passed in to the function is actually a pretty powerful model for encapsulating more complex behavior that might not fit into the methods we could define on a context object.

brysonian commented 10 years ago

+1 for something along the lines of that method of initialization in processing.js. I also like that Processing.js allows you to have a Sketch (Processing.Sketch) object that might not currently be active. So for example you do:

var sketch      = new Processing.Sketch()
sketch.attachFunction = function(p) {
    p.setup = function() {
        // code using p as Processing namespace...  
    }

    p.draw = function() {
        // code using p as Processing namespace...  
    }
}

to create a sketch, and then attach it to a canvas and run it with:

var pro = new Processing(canvas, sketch);

I'm not particularly fond of the name "attachFunction" but like the gist of it.

As for sketches that deal mostly with the DOM and don't draw. Maybe this is where having an "update" function that is called before draw could be handy. Though I also imagine a lot of those cases will be more about responding to user events?

lmccart commented 10 years ago

I'm also in favor of the the pass-a-context-object variant, just wondering if it's a bit much for someone making the transition from global context to wrap their head around? Maybe at this point we assume the user knows js reasonably well and it's not a big deal.

And having the node element be outside of the object definitely makes sense, I wasn't thinking before!

evhan55 commented 10 years ago

Can you all expand on the argument for passing the Processing instance to the function? I am not opposed, but would like to understand the use cases and what makes it powerful.

evhan55 commented 10 years ago

:+1: to the transition aspect, it would be nice if it were a clear transition from global to instance use case. I am working towards coming up with a set of instantiation examples , global and non-global, for you all to look at and verify before I actually implement anything

brightredchilli commented 10 years ago

If I can add some two cents here... Good javascript coding conventions not withstanding, I found the old processing JS to be really unwieldy when having to do "p.frameRate", and "p.random", and then when you get to "new p.PVector(...)" ... then you really want to pull your hair out.

I understand why polluting global namespace is a bad idea, but I really think it comes at the cost of sacrificing readability. Maybe you guys figured out a two-mode option in which case, just disregard this comment : )

On Wed, Feb 19, 2014 at 5:40 PM, Evelyn Eastmond notifications@github.comwrote:

[image: :+1:] to the transition aspect, it would be nice if it were a clear transition from global to instance use case. I am working towards coming up with a set of instantiation examples , global and non-global, for you all to look at and verify before I actually implement anything

Reply to this email directly or view it on GitHubhttps://github.com/lmccart/p5.js/issues/113#issuecomment-35559003 .

kadamwhite commented 10 years ago

@brightredchilli, my understanding from @evhan55 and @lmccart is that we want to support a global mode out-of-the-box, and have a clear transition path from there to using the safer (though more unwieldy) object.method syntax. For my part, I could see a web IDE providing the user with a window where they can type "rect" into a text box and have the IDE bind the command to the right context on the fly, so in the future the only time you'd be 100% required to use the p.method syntax would be if you were hand-authoring a P5 sketch that would be running embedded on a page with other sketches or other code that would preclude using the global mode. I'd like to see this library help people learn good patterns for writing regular JavaScript, but it's still intended to be beginner-friendly at the same time.

kadamwhite commented 10 years ago

@evhan55 you need access to the instance object within the function in order to write commands—if we're clever with our binding that can be accomplished using this, but passing in the processing instance as an argument lets users name it arbitrarily and avoids some of the mess you can get in to if you're using this and suddenly write a classic event handler or something that redefines what that means.

lmccart commented 10 years ago

@brightredchilli yep, that's right. check out @evhan55's original post at the top. this is how things will look/work when you're in "global mode", ie not explicitly instantiating a p5 instance. the syntax around this part is pretty much settled, now we're hashing out the details for the advanced mode, or namespaced mode in which you create a p5 instance yourself. this allows it to play nice with a wider js world and allows more control.

brysonian commented 10 years ago

Also passing the instance makes managing multiple sketches more clear without requiring globals. Similarly I like passing a function, who is in turned passed the instance (ala attachFunction) rather than an object because it allows clearer organization of sketches without pollution. The main case here is defining classes or other functions. If you pass an object with draw and setup, then what do you do with your functions? Define them in a closure in the same scope or something, but that feels a bit convoluted. Whereas something like what @kadamwhite had:

P5(function( pInstance ) {
  pInstance.draw = function() { /* ... */ };
});

Encourages you to define all that in the anon function you pass to P5()

kadamwhite commented 10 years ago

The advantage to passing an object, on the other hand, is that

P5({
  setup: function( canvas ) { /* ... */ }
});

is less to type (and therefore less for beginners to memorize), and from looking around at tutorials setup & draw will let people get by for some time before needing to dive in to closures and such. That said, I play this largely as devil's advocate, since I'm not convinced supporting ALL of the above instantiation methods is wise: to quote, "if we build it, they will come," and then we're tied to supporting umpteen instantiation methods. This may be a good thing, but it is also a decision to be made purposefully.

evhan55 commented 10 years ago

I was going to propose instantiation cases for the non-global case, but I think @kadamwhite 's summary up above in this comment sums up the options pretty well.

Is it too much to say we will support both the object (P5({setup: /* ... */, draw: /* ... */})) and function (P5(function(instance){instance.setup = /* ... */; instance.draw = /* ...*/;})) options?

I prefer the object option because, as @kadamwhite said, it is less to type and easier to look at and wrap your head around, for beginners. In terms of adding more functions to it, why can't you just make an object with more properties and pass that in instead? Or bind the object to a variable outside of the instantiation and pass it in after it's been defined?

var p5Obj = {};
p5Obj.setup = /* ... */;
p5Obj.draw = /* ... */;
p5Obj.mousePressed = /* ... */;
var p5Instance = P5(p5Obj);

I might totally be missing something obvious because I am not very experienced with processing.js.

brysonian commented 10 years ago

I guess i'm not totally convinced it is easier. Here is a sketch that is close to something i did recently in processing.js (see it running here: http://bl.ocks.org/brysonian/9341178)

var sketch = new Processing.Sketch();
sketch.attachFunction = function(p) {

  var walkers = [];
  var walkerNum = 900;

  p.setup = function() {
    p.size(1024, 768);
    p.smooth();
    p.noStroke();

    for (var i=0; i<walkerNum; i++) {
      walkers[i] = new Walker(Math.floor(Math.random() * p.width), Math.floor(Math.random() * p.height));
    }
    p.background(255,255);
  }

  p.draw = function() {
    for (var i=0; i<walkers.length; i++) {
      walkers[i].display();
      walkers[i].move();
    }
  }

  function Walker(ax, ay) {
    this.x = ax;
    this.y = ay;
    this.clr = Math.random() < .4 ? 255 : 0
  }

  Walker.prototype.display = function() {
    p.fill(this.clr, 10)
    p.ellipse(this.x, this.y, 20, 20);
  }

  Walker.prototype.move = function() {
    this.x += p.random(-1, 1);
    this.y += p.random(-1, 1);
  }

}
new Processing(document.getElementById('sketch'), sketch);

What i like about passing the "p" is that it is clear what is a processing.js call and what is just javascript or my code. And i like that once i get into the body of attachFunction, it just looks like a pretty normal bunch of javascript, (presumably like i would have seen in earlier lessons?) It also keeps everything nice and tidy in a closure.

The pass-an-object option on the other hand might look like this (if i'm tracking correctly):

var sketch = {};
sketch.walkers = [];
sketch.walkerNum = 900;
sketch.setup = function() {
  size(1024, 768);
  smooth();
  noStroke();

  for (var i=0; i<this.walkerNum; i++) {
    this.walkers[i] = new Walker(Math.floor(Math.random() * width), Math.floor(Math.random() * height));
  }
  background(255,255);
}

sketch.draw = function() {
  for (var i=0; i<this.walkers.length; i++) {
    this.walkers[i].display();
    this.walkers[i].move();
  }
}

sketch.Walker = function(ax, ay) {
  this.x = ax;
  this.y = ay;
  this.clr = Math.random() < .4 ? 255 : 0
}

sketch.Walker.prototype.display = function() {
  fill(this.clr, 10)
  ellipse(this.x, this.y, 20, 20);
}

sketch.Walker.prototype.move = function() {
  this.x += random(-1, 1);
  this.y += random(-1, 1);
}

var p5Instance = P5(sketch);

Which isn't necessarily better other than you don't have to preface p5.js commands with p, but you do have to preface everything else with your sketch object, and use "this" to access functions and variables defined elsewhere on the object, rather than letting your variable scope do the work for you (in setup and draw that is) and of course "this.Walker" which is maybe extra confusing (of course one could not care about polluting global and make that clearer).

I dunno, both i guess is maybe the best way to go?

bobholt commented 10 years ago

I strongly recommend choosing one and only one way of doing this. Supporting both (or more) would be sacrificing readability, maintainability, and predictability of the source while giving a very small (and questionable) benefit of multiple ways of creating a sketch.

I agree with @brysonian that the function form is more readable when using shared variables/properties. Though I'm not clear about the reasoning behind creating an object with an property that is this function instead of passing the function directly. On Mar 4, 2014 1:49 AM, "Chandler McWilliams" notifications@github.com wrote:

I guess i'm not totally convinced it is easier. Here is a sketch that is close to something i did recently in processing.js (see it running here: http://bl.ocks.org/brysonian/9341178)

var sketch = new Processing.Sketch();sketch.attachFunction = function(p) {

var walkers = []; var walkerNum = 900;

p.setup = function() { p.size(1024, 768); p.smooth(); p.noStroke();

for (var i=0; i<walkerNum; i++) {
  walkers[i] = new Walker(Math.floor(Math.random() * p.width), Math.floor(Math.random() * p.height));
}
p.background(255,255);

}

p.draw = function() { for (var i=0; i<walkers.length; i++) { walkers[i].display(); walkers[i].move(); } }

function Walker(ax, ay) { this.x = ax; this.y = ay; this.clr = Math.random() < .4 ? 255 : 0 }

Walker.prototype.display = function() { p.fill(this.clr, 10) p.ellipse(this.x, this.y, 20, 20); }

Walker.prototype.move = function() { this.x += p.random(-1, 1); this.y += p.random(-1, 1); }

}new Processing(document.getElementById('sketch'), sketch);

What i like about passing the "p" is that it is clear what is a processing.js call and what is just javascript or my code. And i like that once i get into the body of attachFunction, it just looks like a pretty normal bunch of javascript, (presumably like i would have seen in earlier lessons?) It also keeps everything nice and tidy in a closure.

The pass-an-object option on the other hand might look like this (if i'm tracking correctly):

var sketch = {};sketch.walkers = [];sketch.walkerNum = 900;sketch.setup = function() { size(1024, 768); smooth(); noStroke();

for (var i=0; i<this.walkerNum; i++) { this.walkers[i] = new Walker(Math.floor(Math.random() * width), Math.floor(Math.random() * height)); } background(255,255);} sketch.draw = function() { for (var i=0; i<this.walkers.length; i++) { this.walkers[i].display(); this.walkers[i].move(); }} sketch.Walker = function(ax, ay) { this.x = ax; this.y = ay; this.clr = Math.random() < .4 ? 255 : 0} sketch.Walker.prototype.display = function() { fill(this.clr, 10) ellipse(this.x, this.y, 20, 20);} sketch.Walker.prototype.move = function() { this.x += random(-1, 1); this.y += random(-1, 1);} var p5Instance = P5(sketch);

Which isn't necessarily better other than you don't have to preface p5.js commands with p, but you do have to preface everything else with your sketch object, and use "this" to access functions and variables defined elsewhere on the object, rather than letting your variable scope do the work for you (in setup and draw that is) and of course "this.Walker" which is maybe extra confusing (of course one could not care about polluting global and make that clearer).

I dunno, both i guess is maybe the best way to go?

Reply to this email directly or view it on GitHubhttps://github.com/lmccart/p5.js/issues/113#issuecomment-36596667 .

kadamwhite commented 10 years ago

+1 to @bobholt, and some thoughts in defense of the function variant (despite what I said before):

P5(function( p ) {
  p.anyMethod(); // obvious: you see where "p" comes from and how to use it
  // "p" can also be changed to a more descriptive name.
  // We could even go 100% whole-word:
  //
  //     P5(function( sketch ) {
  //       sketch.rectangle( 0, 0, 100, 100 );
  //     });
});

The Object version involves less typing, but still requires calling the functions on some object (something.drawMethod()) and if that "something" is this, that feels a lot more "magic":

P5({
  setup: function() {
    // "this"? Huh? What does "this" mean?
    this.background( 0, 100, 100 );
  }
});

// or alternatively, if we pass in the PGraphics instance as we'd considered:
P5({
  setup: function( sketch ) {
    // A learner asks: I saw you use "this" earlier...
    // is "canv" the same as "this"?
    sketch.background( 0, 100, 100 );
  }
});

The benefit to the function method is that while it requires more typing and more abstraction, it limits the amount of crazy binding we'd need to do and I hypothesize that could keep things much more straight-forward for beginners. We're envisioning these as the next step up from the global version, but I still think it might be a bit early to toss them into this.

evhan55 commented 10 years ago

In the function example below, I can see the beginner asking:

What does function mean?

Why am I writing a function now? "It's a wrapper for the sketch" But I thought P5() was the wrapper?

Where does 'p' come from? When is it bound to anything? Isn't P5 the sketch, why is there another sketch?

The abstraction is very deep here, and this reduces the need for this, but might add a huge barrier. The barrier might be fine, but I would want an elegant story to tell students. During Scratch development, we had members on the team who always advocated for the beginner, and I find myself playing that role here just out of habit. So this is why I'm bringing up all the questions now, that's all!

P5(function( p ) {
  p.anyMethod(); // obvious: you see where "p" comes from and how to use it
  // "p" can also be changed to a more descriptive name.
  // We could even go 100% whole-word:
  //
  //     P5(function( sketch ) {
  //       sketch.rectangle( 0, 0, 100, 100 );
  //     });
});
lmccart commented 10 years ago

hm I'm sort of split, both seem ok, but I agree let's just choose one.

I think learning programming and javascript especially often involves some leaps of faith. similar to when we introduce classes and just have students memorize the prototype form at the beginning rather than diving deeply into an explanation, maybe this would be something of the same?

memorize the first line rather than trying to wade through several layers of abstraction, and you can start with pretty clean and straightforward innards.

good to think about the beginner, but a few more questions to weigh on the decision:

  1. are either of these options significantly easier to implement or maintain?
  2. is one of these implementations closer to what we currently have?
  3. does one fit more naturally with processing.js (minor point bc I think both are close but not exact as we're ditching the attachFunc, so we will need a shim regardless, right?)
brysonian commented 10 years ago

@evhan55 I fully agree that having a clear narrative is vital to getting the concepts across, and that choosing syntax to support that is the way to go. I would say that I’ve noticed students often have an easier time with the concepts and abstractions that are required than they have with the syntax (teaching processing arrays is a perfect example here, and one reason i can’t wait to move to js!) 

With the narrative in mind, my last thought is something like this:

var sketch = new P5.Sketch(); // or P5.createSketch();
sketch.setup = function() { ... }
sketch.draw = function() { ... }
P5.run(sketch); // P5.run(sketch, canvasElement);

It makes a clear distinction between the concept of a “sketch” and P5 itself which is more of a manager. The syntax is familiar from making JavaScript objects.  It makes it clear that defining a sketch and running the sketch need not be the same thing.

Eager to see what y’all settle on!


Chandler McWilliams http://brysonian.com

On March 4, 2014 at 7:33:57 AM, Evelyn Eastmond (notifications@github.com) wrote:

In the function example below, I can see the beginner asking:

What does function mean?

Why am I writing a function now? "It's a wrapper for the sketch" But I thought P5() was the wrapper?

Where does 'p' come from? When is it bound to anything? Isn't P5 the sketch, why is there another sketch?

The abstraction is very deep here, and this reduces the need for
this, but might add a huge barrier. The barrier might be fine,
but I would want an elegant story to tell students. During Scratch
development, we had members on the team who always advocated
for the beginner, and I find myself playing that role here just
out of habit. So this is why I'm bringing up all the questions now,
that's all!

P5(function( p ) {
p.anyMethod(); // obvious: you see where "p" comes from and how  
to use it
// "p" can also be changed to a more descriptive name.
// We could even go 100% whole-word:
//
// P5(function( sketch ) {
// sketch.rectangle( 0, 0, 100, 100 );
// });
});

Reply to this email directly or view it on GitHub: https://github.com/lmccart/p5.js/issues/113#issuecomment-36636534

evhan55 commented 10 years ago

@brysonian

I really, really (really) like the clarity of that and would vote for P5.createSketch();

What do others think of @brysonian 's proposal? @lmccart @kadamwhite @tafsiri @bobholt etc..

hamoid commented 10 years ago

Wouldn't

sketch.setup = function() { ... } 
sketch.draw = function() { ... } 
sketch.run(); // sketch.run(canvasElement);

be more intuitive than

sketch.setup = function() { ... } 
sketch.draw = function() { ... } 
P5.run(sketch); // P5.run(sketch, canvasElement);

? Or does it need to happen through the P5 object?

brysonian commented 10 years ago

@hamoid i would worry that the sketch's run method would get overwritten and cause a hard to find bug

sketch.run = function() { ... }
lmccart commented 10 years ago

Ok, great thoughts all. @evhan55 and I have gone through all the options and tried to distill the best options. https://gist.github.com/lmccart/9399632

There are three main options, each of them have some options between this vs a named variable, which would be functionally equivalent and thus supported, but we also want to think about which one we'd present as the template for teaching.

tafsiri commented 10 years ago

One small comment about P5.sketch() vs P5.createSketch(). I think if you use the later you should drop the 'new'. the createXXX are more factory like methods as opposed to constructors themselves. So either new P5.sketch() or P5.createSketch would be my suggestion.

Otherwise I like option 2. Its pretty straightforward, and delays needing to know what 'this' is for a bit (though still allowing that to be taught later).

I also see @hamoid point about sketch.run() being a bit clearer than P5.run() (at least to me). In terms of the risk of run() being overwritten, that is probably true for any of the methods on the sketch object (background, etc). I don't feel terribly strongly about this either way, (whichever makes the internals cleaner would probably take my vote on that one).

evhan55 commented 10 years ago

@tafsiri Oh! I believe the 'new' was left in there by accident!

bobholt commented 10 years ago

I agree with @tafsiri, with the though I would prefer to see either new P5() or P5.createSketch() for option 2. From a transparency perspective, I would prefer to instantiate an object with the new keyword, instead of hiding it in a factory method, but can see where createSketch() is more "verb-y."

And I agree sketch.run() is clearer. Overwriting prototype methods is always an issue with JavaScript, and one we shouldn't shy away from. It also allows some pretty powerful things, like if a power user were to want to monkey-patch the run method to add functionality for their sketches in the future. Good documentation, explanation, and coding practices (e.g. "Don't write a method called sketch.run(). It would be bad.") could keep this from being an issue.

On Fri, Mar 7, 2014 at 12:59 AM, Evelyn Eastmond notifications@github.comwrote:

@tafsiri https://github.com/tafsiri Oh! I believe the 'new' was left in there by accident!

Reply to this email directly or view it on GitHubhttps://github.com/lmccart/p5.js/issues/113#issuecomment-36970288 .