Repository: processing/p5.js (JavaScript)

Issue:
Texture from IMG element does not use actual dimensions of image
### Most appropriate sub-area of p5.js?

- [ ] Accessibility (Web Accessibility)
- [ ] Build tools and processes
- [ ] Color
- [ ] Core/Environment/Rendering
- [ ] Data
- [X] DOM
- [ ] Events
- [ ] Friendly error system
- [X] Image
- [ ] IO (Input/Output)
- [ ] Localization
- [ ] Math
- [ ] Unit Testing
- [ ] Typography
- [ ] Utilities
- [X] WebGL
- [ ] Other (specify if possible)

### p5.js version

1.4.1

### Web browser and version

Version 1.36.112 Chromium: 99.0.4844.51 (Official Build) (x86_64)

### Operating System

MacOS 12.3.1

### Steps to reproduce this

### Steps:
1. Use a file input to upload an image of power-of-two size (like 1024x1024) into an existing \<img\> element that has a non-power-of-two size. Like 40x40.
2. Use that \<img\> element as a texture with a texture wrap mode of REPEAT.
3. p5js warns that the image is not a power of two and that wrap mode will be changed to CLAMP.

Problem seems to be that it's using the width and height attributes of the \<img\> element and should be using the naturalWidth and naturalHeight attributes.


Discussion:
Welcome! 👋 Thanks for opening your first issue here! And to ensure the community is able to respond to your issue, be sure to follow the issue template if you haven't already.

Hey, I'm having a similar issue with textureWrap()
Did you find a workaround to this problem?
No I didn't find an elegant workaround. I ended up loading the image twice (once through the img element and once with loadimage). I did look through the p5.js code to see if it would be a simple fix. It's not. The width/height are used quite generically so it would be a bigger fix to suss out when to use naturalWidth/Height instead. I'm neck deep in a project right now so I can't volunteer to fix it (yet).
I think there's probably a couple ways to solve this issue. First, you could always draw your html element to another createGraphics() layer that is resized to be power of two, and then draw it back at the correct aspect ratio when you go to use it.

Second, you can approximate texture wrapping in a shader quite easily using the fract function.

For wrapping:
```
vec4 color = texture2D(mySampler, fract(uv * numTiles));
```

If you can share more code snippets I could probably give more clear assistance. Hope that helps!
Hello, my solution to this issue was drawing my cylinder with beginShape() and a little bit of maths. Texture wrap worked for me in the case, hope that helps!
I just double checked and confirmed that if we override the existing `p5.Texture` functions and make them use `naturalWidth` and `naturalHeight`, then [textures successfully wrap.](https://editor.p5js.org/davepagurek/sketches/IXgpMfbO-) So I agree that the fix for this would be to update [the constructor](https://github.com/processing/p5.js/blob/ea0fa1d270003437c2750da0438d48e93954ea02/src/webgl/p5.Texture.js#L92) and [the `update` function](https://github.com/processing/p5.js/blob/ea0fa1d270003437c2750da0438d48e93954ea02/src/webgl/p5.Texture.js#L188) to check `naturalWidth` and `naturalHeight` first if it's an image element, and fall back to `width`/`height` otherwise.
Hi, I took a look at the demo and it seems that `this.width` and `this.height` are being set to `undefined`. It seems that setting the width and height to any power of 2 will cause the texture to wrap properly.

> I just double checked and confirmed that if we override the existing `p5.Texture` functions and make them use `naturalWidth` and `naturalHeight`, then [textures successfully wrap.](https://editor.p5js.org/davepagurek/sketches/IXgpMfbO-) So I agree that the fix for this would be to update [the constructor](https://github.com/processing/p5.js/blob/ea0fa1d270003437c2750da0438d48e93954ea02/src/webgl/p5.Texture.js#L92) and [the `update` function](https://github.com/processing/p5.js/blob/ea0fa1d270003437c2750da0438d48e93954ea02/src/webgl/p5.Texture.js#L188) to check `naturalWidth` and `naturalHeight` first if it's an image element, and fall back to `width`/`height` otherwise.


@leslieyip02 thanks for catching that! My mistake, the inside of the branch was missing the `elt.` prefix. I've updated the sketch to now have this:

```js
if (this.src.elt && this.src.elt.naturalWidth && this.src.elt.naturalHeight) {
  this.width = this.src.elt.naturalWidth
  this.height = this.src.elt.naturalHeight
}
```

> It seems that setting the width and height to any power of 2 will cause the texture to wrap properly.

Setting the width/height to any power of 2 will just bypass p5's check before asking WebGL to repeat the texture. It seems that browsers can still support REPEAT for all sizes sometimes. [MDN says this though, about the spec in general:](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Using_textures_in_WebGL)

> if the texture is a power of 2 in both dimensions then WebGL can do higher quality filtering, it can use mipmap, and it can set the wrapping mode to REPEAT or MIRRORED_REPEAT.

But based on that description, it sounds like the spec only *requires* this to work for power of 2 sizes, so it's probably best to just keep p5's check in place anyway, just to be safe, but basing it on `this.src.elt.naturalWidth`.
@davepagurek I saw the Good First Issue label and tried implementing the change suggested to the power of 2 check but found that this.src.elt.naturalWidth didn't seem to exist for all cases which I think you mentioned here
> first if it's an image element, and fall back to width/height otherwise.

I would love to work on a PR for this
@kate-grant right, [according to MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/naturalWidth) it can sometimes be 0 when it exists, so we should probably check if both exist and are truthy before using them.

> I would love to work on a PR for this

Thanks! I'll assign this issue to you 🙂 Let me know if you run into any problems!
@davepagurek I had to solve this in a different way than the original recommended solution. The original solution made changes to the constructor, changing `this.width` and `this.height`, but this interfered with the update function in some cases [`data.setModified(true);`](https://github.com/processing/p5.js/blob/main/src/webgl/p5.Texture.js#L207). Instead I utilized this logic in `setWrapMode` with new variables `wrapWidth` and `wrapHeight` for use in the `isPowerOfTwo` check. Let me know if you think this is ok.

I added two unit tests for textureWrap REPEAT on textures from image elements for power-of-two and non-power-of-two (the power-of-two case should allow REPEAT and the non-power-of-two case should set to CLAMP). I had to add two things to the Texture unit test setup fn--let me know if this is ok.



Patch:
diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js
index 3f92678a49..3dc0df6271 100644
--- a/src/webgl/p5.Texture.js
+++ b/src/webgl/p5.Texture.js
@@ -334,9 +334,21 @@ p5.Texture.prototype.setWrapMode = function(wrapX, wrapY) {
   // if it isn't we will set the wrap mode to CLAMP
   // webgl2 will support npot REPEAT and MIRROR but we don't check for it yet
   const isPowerOfTwo = x => (x & (x - 1)) === 0;
+  const textureData = this._getTextureDataFromSource();
+
+  let wrapWidth;
+  let wrapHeight;
+
+  if (textureData.naturalWidth && textureData.naturalHeight) {
+    wrapWidth = textureData.naturalWidth;
+    wrapHeight = textureData.naturalHeight;
+  } else {
+    wrapWidth = this.width;
+    wrapHeight = this.height;
+  }
 
-  const widthPowerOfTwo = isPowerOfTwo(this.width);
-  const heightPowerOfTwo = isPowerOfTwo(this.height);
+  const widthPowerOfTwo = isPowerOfTwo(wrapWidth);
+  const heightPowerOfTwo = isPowerOfTwo(wrapHeight);
 
   if (wrapX === constants.REPEAT) {
     if (widthPowerOfTwo && heightPowerOfTwo) {


Test patch:
diff --git a/test/unit/webgl/p5.Texture.js b/test/unit/webgl/p5.Texture.js
index 359f98d0b1..3864c3d559 100644
--- a/test/unit/webgl/p5.Texture.js
+++ b/test/unit/webgl/p5.Texture.js
@@ -2,6 +2,9 @@ suite('p5.Texture', function() {
   var myp5;
   var texImg1;
   var texImg2;
+  var texImg3;
+  var imgElementNotPowerOfTwo;
+  var imgElementPowerOfTwo;
   var canvas;
 
   if (!window.Modernizr.webgl) {
@@ -12,16 +15,34 @@ suite('p5.Texture', function() {
   setup(function(done) {
     myp5 = new p5(function(p) {
       p.preload = function() {
+        // texImg2 must have powerOfTwo dimensions
         texImg2 = p.loadImage('unit/assets/target.gif');
-
+        // texImg3 must NOT have powerOfTwo dimensions
+        texImg3 = p.loadImage('unit/assets/nyan_cat.gif');
         // texture object isn't created until it's used for something:
         //p.box(70, 70, 70);
       };
       p.setup = function() {
         canvas = p.createCanvas(100, 100, p.WEBGL);
         texImg1 = p.createGraphics(2, 2, p.WEBGL);
-        p.texture(texImg1);
-        done();
+        new Promise(resolve => {
+          p.createImg(texImg2.canvas.toDataURL(), '', 'anonymous', el => {
+            el.size(50, 50);
+            imgElementPowerOfTwo = el;
+            p.texture(imgElementPowerOfTwo);
+            resolve();
+          });
+        }).then(() => new Promise(resolve => {
+          p.createImg(texImg3.canvas.toDataURL(), '', 'anonymous', el => {
+            el.size(50, 50);
+            imgElementNotPowerOfTwo = el;
+            p.texture(imgElementNotPowerOfTwo);
+            resolve();
+          });
+        })).then(() => {
+          p.texture(texImg1);
+          done();
+        });
       };
     });
   });
@@ -87,6 +108,18 @@ suite('p5.Texture', function() {
       assert.deepEqual(tex.glWrapS, myp5._renderer.GL.MIRRORED_REPEAT);
       assert.deepEqual(tex.glWrapT, myp5._renderer.GL.MIRRORED_REPEAT);
     });
+    test('Set wrap mode REPEAT if src dimensions is powerOfTwo', function() {
+      const tex = myp5._renderer.getTexture(imgElementPowerOfTwo);
+      tex.setWrapMode(myp5.REPEAT, myp5.REPEAT);
+      assert.deepEqual(tex.glWrapS, myp5._renderer.GL.REPEAT);
+      assert.deepEqual(tex.glWrapT, myp5._renderer.GL.REPEAT);
+    });
+    test('Set default wrap mode CLAMP if src dimensions != powerOfTwo', function() {
+      const tex = myp5._renderer.getTexture(imgElementNotPowerOfTwo);
+      tex.setWrapMode(myp5.REPEAT, myp5.REPEAT);
+      assert.deepEqual(tex.glWrapS, myp5._renderer.GL.CLAMP_TO_EDGE);
+      assert.deepEqual(tex.glWrapT, myp5._renderer.GL.CLAMP_TO_EDGE);
+    });
     test('Set textureMode to NORMAL', function() {
       myp5.textureMode(myp5.NORMAL);
       assert.deepEqual(myp5._renderer.textureMode, myp5.NORMAL);