1

I'm trying to update the ImageData of a Canvas's image context, and when I try to set elements in the data array, I get an error saying the array has type Js.Typed_array.Uint8ClampedArray.t when something expected array('a).

Why can't I update a JS TypedArray implementation?

Here's my component code (simplified somewhat for clarity):

let make = _children => {
    let map = FeatureMap.make(100, 100);
    let (width, height) = map.dimensions;

    {...component,
        initialState: () => {
            map: map,
            canvasRef: ref(None)
        },
        didMount: self => switch (self.state.canvasRef^) {
            | None => ()
            | Some(canvas) => {
                let ctx = getContext2d(canvas);
                let imageData = createImageDataCoords(ctx, ~width=float_of_int(width), ~height=float_of_int(height));
                let data = Webapi.Dom.Image.data(imageData);

                Array.iteri((x, row) => {
                    Array.iteri((y, weight) => {
                        let index = (x * width + y) * 4;
                        let (r, g, b) = weight;
                        data[index + 0] = r;
                        data[index + 1] = g;
                        data[index + 2] = b;
                        data[index + 3] = 0;
                    }, row);
                }, map.weights);

                ctx |> putImageData(imageData, 0., 0., 0., 0., float_of_int(width), float_of_int(height));  
            }
        },
        render: _self => <canvas id="weight-map"
                        width={string_of_int(width)}
                        height={string_of_int(width)}
                        ref={_self.handle(setCanvasRef)}></canvas>
    };
};
glennsl
  • 28,186
  • 12
  • 57
  • 75
Brendan Berg
  • 1,918
  • 2
  • 19
  • 22

1 Answers1

3

To the compiler, an array('a) is not the same type as a Js.Typed_array.Uint8ClampedArray.t, hence their operations (including indexing) are not interchangeable. This is the same principle by which you can't add an int and a float.

To set a typed array element, you need to find (or write) a binding that allows you to do that explicitly, rather than using the indexing operator. To do that, you can look in the Js.Typed_array module–there is a module type S which we can take to mean 'all typed array modules must conform to this module signature'. And that includes the Js.Typed_array.Uint8ClampedArray module. So you can use the S module type's unsafe_set function to set the typed array elements, because Js.Typed_array.Uint8ClampedArray implements it:

let module UI8s = Js.Typed_array.Uint8ClampedArray;
UI8s.unsafe_set(data, index, r);
UI8s.unsafe_set(data, index + 1, g);
UI8s.unsafe_set(data, index + 2, b);
UI8s.unsafe_set(data, index + 3, 0);
Yawar
  • 11,272
  • 4
  • 48
  • 80
  • It is also possible to provide indexing operators for typed arrays I think. Since `[]` is just syntax sugar for `Array.get` and `Array.set` you could provide those implementations in `Uint8ClamedArray.Array`, for example, in order to do `Uint8ClampedArray.(data[index + 2] = g)`. But you'd get warnings, I'm pretty sure, and wouldn't be able to avoid the warnings without explicitly disabling them either globally or at every call site. – glennsl Mar 17 '19 at 08:13
  • 1
    @glennsl yup there is also the new indexing operator overload mechanism since 4.06: https://caml.inria.fr/pub/docs/manual-ocaml/extn.html#s%3Aindex-operators – Yawar Mar 17 '19 at 13:57
  • @glennsl part of my confusion was that it doesn't appear that `Uint8ClampedArray` & family have `get` and `set` functions. Does `[]` desugar to `unsafe_get` & `unsafe_set` in that case? – Brendan Berg Mar 17 '19 at 22:09
  • @BrendanBerg yeah that had me scratching my head for a moment. The functions are not showing up in those modules' API docs for some reason. – Yawar Mar 18 '19 at 01:18
  • 1
    @BrendanBerg No, `[]` is statically translated. It knows nothing about types. What I'm saying is that `Array.get` and `.set` _could_ have been provided within each typed array module in order to support the indexing operator, but isn't because using them would emit warnings, which isn't very practical. Also, just renaming `unsafe_get` to `get` wouldn't be sufficient, it also needs to be put within an `Array` module. – glennsl Mar 18 '19 at 08:38
  • Btw, `unsafe_get` is named as it is because it will raise an exception if the index is out of bounds. This naming is consistent with `Js.Array.unsafe_get` which also does and `Belt.Array.get` which doesn't while `Belt.Array.getExn` does. It is not consistent with the standard `Array.get` which for historical reasons also raises an exception without the name giving any indication of that. My hope is that the standard library is replaced with a better alternative. Unfortunately, `Belt`, which is being positioned as the replacement, is also terribly designed even if it does get this right. – glennsl Mar 18 '19 at 08:44
  • @glennsl interesting, I assumed the 'unsafe' was in the naming because these functions don't do bounds-checking. – Yawar Mar 18 '19 at 15:01
  • 1
    @Yawar Ah, yeah you're right of course. Sorry. So `unsafe_get` behaves like `Belt.Array.getUnsafe` in that it'll just do what JavaScript does, which isn't sound, while `Array.get` behaves like `Belt.Array.getExn` which does bounds checking and raises an exception if out of bounds. The ideal safe implementation is to return an `option`, like `Belt.Array.get` does. – glennsl Mar 18 '19 at 15:38
  • 1
    If you're using `TypedArray`, you're probably doing so for performance reasons, and then `unsafe_get` is of course the fastest implementation, but you have to do bounds checking yourself. A better API might have higher level functions that are both safe and fast, but we have to work with what we get. – glennsl Mar 18 '19 at 15:41