P5.js: setting canvas style position property doesn't work

Created on 12 Jan 2019  路  9Comments  路  Source: processing/p5.js

Nature of issue?

  • [x] Found a bug

Most appropriate sub-area of p5.js?

  • [x] Core/Environment/Rendering

Which platform were you using when you encountered this?

  • [x] Desktop/Laptop

Details about the bug:

  • p5.js version: current CDN version
  • Web browser and version: 71.0.3578.98 (Official Build) (64-bit)
  • Operating System: macOS 10.14.2
  • Steps to reproduce this:

Setting the following CSS is the only way to have the canvas bound to the top left corner of the current view.

canvas {
    position: fixed;
    top: 0;
    left: 0;
    z-index: -1;
}

the corresponding setup() function does not work by itself.

function setup() {
    canvas = createCanvas(windowWidth, windowHeight);
    canvas.style('position', 'fixed');
    canvas.style('top', '0');
    canvas.style('left', '0');
    canvas.style('z-index', '-1');
}

So only by setting the styling in CSS does the created canvas actually honour it. When set using canvas.style() the canvas does correctly reposition to the top of the page, however, it does not follow the current scroll position as it does with the CSS method.

documentation

Most helpful comment

@mkmori @nebbles Thanks for reporting this. I've opened up a new issue to document this #4556

All 9 comments

I tried to read the p5.dom.js library file and got the following results(sorry for the long message):

The following is the style function from p5.dom.js:

p5.Element.prototype.style = function(prop, val) {
    var self = this;

    if (val instanceof p5.Color) {
      val =
        'rgba(' +
        val.levels[0] +
        ',' +
        val.levels[1] +
        ',' +
        val.levels[2] +
        ',' +
        val.levels[3] / 255 +
        ')';
    }

    if (typeof val === 'undefined') {
      if (prop.indexOf(':') === -1) {
        var styles = window.getComputedStyle(self.elt);
        var style = styles.getPropertyValue(prop);
        return style;
      } else {
        var attrs = prop.split(';');
        for (var i = 0; i < attrs.length; i++) {
          var parts = attrs[i].split(':');
          if (parts[0] && parts[1]) {
            this.elt.style[parts[0].trim()] = parts[1].trim();
          }
        }
      }
    } else {
      if (prop === 'rotate' || prop === 'translate' || prop === 'position') {
        var trans = Array.prototype.shift.apply(arguments);
        var f = this[trans] || this['_' + trans];
        f.apply(this, arguments);
      } else {
        this.elt.style[prop] = val;
        if (
          prop === 'width' ||
          prop === 'height' ||
          prop === 'left' ||
          prop === 'top'
        ) {
          var numVal = val.replace(/\D+/g, '');
          this[prop] = parseInt(numVal, 10); // pend: is this necessary?
        }
      }
    }
    return this;
  };


since the prop 'position' results in invoking another function which is:

 p5.Element.prototype.position = function() {
    if (arguments.length === 0) {
      return { x: this.elt.offsetLeft, y: this.elt.offsetTop };
    } else {
      this.elt.style.position = 'absolute';
      this.elt.style.left = arguments[0] + 'px';
      this.elt.style.top = arguments[1] + 'px';
      this.x = arguments[0];
      this.y = arguments[1];
      return this;
    }
  };

since ('position', 'fixed') will be finally passed as an arguments array of size 1 to the position function, and there is no case to handle that condition , it is not getting applied.

I would love to help on this issue!

Had a look into this and a further dig around finally led me to a Reddit post which tried out a manual override.

So I tried

function setup() {
    canvas = createCanvas(windowWidth, windowHeight);

    // canvas.style('position', 'fixed');
    canvas.elt.style.position = 'fixed'; // manually set the style
    canvas.style('top', '0');
    canvas.style('left', '0');
    canvas.style('z-index', '-1');
}

which worked as it should do.

So it seems the problem is indeed narrowed down to the p5.Element.prototype.position you found not assigning the styling properly.

The question is, was this by design? Or was it an oversight from the developers? If it is indeed a bug, I'd be happy to attempt providing a PR for it.

This is very similar to the issue #2630,
and it seems the best solution , adhering to the existing reference and code structure , would be
to set the position css using :

elem.style('position: relative'); //passing as a string

as the reference states that when the first argument is 'rotate', 'translate', or 'position',
it takes only numbers as the values.
May be we can modify the reference , to make it clear how to pass values for these special arguments.

So when I put together to the PR #3448 to address this issue, it seemed most appropriate to update the p5.Element.prototype.position since style() was calling that (along with the corresponding ones for rotation, etc.).

It seems, however, that it is actually style() that is in the wrong by calling position so I think my correction will probably need to be moved there.

All that aside, it is clear from #2630, and the documentation for the style() method, that style() is the one misbehaving:

   * Sets the given style (css) property (1st arg) of the element with the
   * given value (2nd arg). If a single argument is given, .style()
   * returns the value of the given property; however, if the single argument
   * is given in css syntax ('text-align:center'), .style() sets the css
   * appropriately. .style() also handles 2d and 3d css transforms. If
   * the 1st arg is 'rotate', 'translate', or 'position', the following arguments
   * accept numbers as values. ('translate', 10, 100, 50);

I will update my PR #3448 when I have a moment to fix style instead of position.

I think if style() with 'position' as its first parameter would call p5.Element.prototype.position it is duplicating functionality and make using positioning other than absolute confusing if not difficult (style('position', 10, 100) and style('position: fixed') looking similar but drastically different pactically).

As mentioned in the PR, I think style() should deal with CSS directly as that is what it does for all other labels (1st argument to style()) but not position which is inconsistent. The documentation may include position as one of the special case but I think that should be amended as well to exclude position.

I agree with @limzykenneth and my proposal would be to remove the special handling of rotate, translate, and position. That would mean removing this section:

      if (prop === 'rotate' || prop === 'translate' || prop === 'position') {
        var trans = Array.prototype.shift.apply(arguments);
        var f = this[trans] || this['_' + trans];
        f.apply(this, arguments);
      }

I think this was an effort to provide easy access to some of the css transform properties but there are many of them and it's not quite clear why these ones have special handling while others don't. We could simplify style() to have only two argument formats: either a single string or a key, value pairing.

Hi: I ran into something maybe related, where the cryptic error results from missing units when setting width and height via style('attribute', value) syntax:

function setup() {
  createCanvas(400, 400);

  let div0 = createDiv('hello');
  div0.style('width', 200+'px'); //works
  div0.style('height', 300); //Uncaught TypeError: val.replace is not a function (sketch: line 6)
  div0.position(50, 50, 'absolute');
}

Not sure this is something to be fixed or not, but some assignments seem to default to 'px' when passed a unit-less value. I also noticed, style() assignment seems to work for width and height, whereas attribute() assignment does not...? Good chance it's all newbie user error, but thought I'd mention it! FWIW!

@mkmori You are passing a value of type number to the style() method, hence why you are getting a error. Your error is saying that it is trying to use the .replace() method (which only exists for variables of type string).

Change line to

div0.style('height', '300'); // passing the value as a string, not a number

This should probably be fixed though in the source though to always attempt converting the value to string (to protect the newbies a little better!)


For extra context, the line above it works because when you concatenate a number with a string (300+'px') then the result always ends up as a string type i.e. '300px' (Javascript)

@mkmori @nebbles Thanks for reporting this. I've opened up a new issue to document this #4556

Was this page helpful?
0 / 5 - 0 ratings

Related issues

hsab picture hsab  路  28Comments

workergnome picture workergnome  路  32Comments

makeyourownalgorithmicart picture makeyourownalgorithmicart  路  23Comments

lmccart picture lmccart  路  49Comments

stalgiag picture stalgiag  路  23Comments