Substitute for CSS Hue-Rotate

I created a Resource in javascript as a substitution for using the CSS Hue-Rotate function, which I feel doesn’t accurately represent the colors I’m seeing in real life.
I got it working great on one Dashboard. But when I use the same resource on a new dashboard, it doesn’t seem to be getting the State of the Entity. I’ve actually cobbled this together a while ago, and thought I had it working pretty well. But my knowledge of how this all works is limited. I started on this site https://github.com/home-assistant-tutorials and went from there.
The problem I seem to be having when I call this on a new dashboard is the first line in the getCanvasInfo() function. I believe this._state comes back as undefined and therefore defaults to ‘unavailable’
Hopefully I explained that well.
I don’t know if anyone with more experience and knowledge can figure out what I’m doing wrong.

I’ve included the first part of my code. Hopefully formatted properly.

class ImageHueRotateCard extends HTMLElement {

    //private properties
    _config;
    _hass;
    _elements = {};
    _state;
    _imWidth;
    _imHeight;

    constructor() {
        super();
        this.createCard();
        this.createStyle();
        this.attachCard();
        this.setElements();   
    }

    setConfig(config) {
        if (!config.entity) {
            throw new Error("You need to define an entity");
        }
        if (!config.state_image || !config.state_image["on"]) {
            throw new Error("You need to define an 'ON' state image");
        } 
        if (!config.state_image["off"]) {
            throw new Error("You need to define an 'OFF' state image");
        }
        if (!config.state_image["unavailable"]) {
            throw new Error("You need to define an unavailable state image");
        }

        this._config = config;
    }

    set hass(hass) {
        this._hass = hass;
        this.updateHass();
    }

    onClicked() {
        this.doToggle();
    }

    connectedCallback() {
        this.getCanvasInfo();
    }

    createCard() {
        this._elements.card = document.createElement('ha-card');
        this._elements.card.innerHTML = `
            <div class="ihr-card">
                <canvas class="c1" id ="canvas1" style="display: block">
                </canvas>
            </div>
        `;
    }

    createStyle() {
        this._elements.style = document.createElement('style');
        this._elements.style.textContent = `
            .type-custom-image-canvas-card {
                padding: 0 0 0 0;
                margin: 0 0 0 0;
                overflow: hidden;
            }
            .ihr-card {
                padding: 0 0 0 0;
                margin: 0 0 0 0;
                overflow: hidden;
            }
        `
    }
    attachCard(){
        this.attachShadow({mode: 'open'});
        this.shadowRoot.append(this._elements.style, this._elements.card);
    }

    setElements() {
        const card = this._elements.card;
        this._elements.dimSrc = card.querySelector(".ihr-card");
        this._elements.dimSrc.addEventListener("click", this.onClicked.bind(this), false);
    }

    updateHass() {
        const entityID = this._config.entity;
        this._state = this._hass.states[entityID];
    }

    getEntityID() {
        return this._config.entity;
    }
    
    doToggle() {
        this._hass.callService('light', 'toggle', {
            entity_id: this.getEntityID()
        });
    }

    getImageWidth(src) {
		return new Promise((resolve) => {
			const img = new Image();
			img.onload = () => resolve (img.width);
			img.src = src;
		});
	}

	getImageHeight(src) {
		return new Promise((resolve) => {
			const img = new Image();
			img.onload = () => resolve (img.height);
			img.src = src;
		});
	}

    async getCanvasInfo() {
        const stateStr = this._state ? this._state.state : 'unavailable';
        
        if (stateStr == 'on') {
            var imageSRC = this._config.state_image["on"];
        } else if (stateStr == 'off') {
            var imageSRC = this._config.state_image["off"];
        } else {
            var imageSRC = this._config.state_image["unavailable"];
        }
        console.log("The Entity is " + this._config.entity);
        console.log("The State is "+ stateStr);

        this._imWidth = await this.getImageWidth(imageSRC);
		this._imHeight = await this.getImageHeight(imageSRC);
	            
        const cardSource = this._elements.dimSrc
        const cardSize = cardSource.getBoundingClientRect();
        
        var w = cardSize.width;
        var h = this._imHeight * w / this._imWidth;

        console.log("Image Size is: " + this._imWidth + " x " + this._imHeight)
        console.log("Canvas Size is: " + w + " x " + h);

        this.drawCanvas(imageSRC, w, h, stateStr, cardSource);
		
    }
    drawCanvas(src, w, h, state, card){
        const canvas = this._elements.card.querySelector('.c1');
	    const ctx = canvas.getContext('2d',{willReadFrequently: true});
        
        const image1 = new Image();
        image1.src = src;

        ctx.canvas.width = w;
		ctx.canvas.height = h;
		ctx.drawImage(image1, 0, 0, w, h);

        if(state == 'on'){
            this.changeHue(ctx, w, h);
        }

        setInterval(() => {
            let currentCardSize = card.getBoundingClientRect();
            if (currentCardSize.width != w) {
                this.reDrawCanvas(ctx, currentCardSize.width, w, h, image1, state);
                w = currentCardSize.width;
                h = this._imHeight * w / this._imWidth;;
            }           
        })
    }

    reDrawCanvas(ctx, newW, w, h, img, state) {
		var scale = newW / w;
        var newH = h * scale;
		ctx.canvas.width = newW;
		ctx.canvas.height = newH;
		ctx.scale(scale, scale);
		ctx.drawImage(img, 0, 0, w, h);
        if(state == 'on'){
            this.changeHue(ctx, w, h);
        }
		ctx.scale(-scale, -scale);
	}

This sounds more like a browser incompatibility.
Not all browsers support the same javascript features and sometimes they might support it, but do it differently.

Interesting. I did load the new page on Firefox and it worked! Although when I looked at the console output, it still showed the State as unavailable, yet the page loaded as expected.
The other weird thing is, my code worked fine in Chrome when I have a page full of overlays using my custom:image-hue-rotate-card, but when I was trying a new page with just one instance, it wouldn’t load the ‘on’ image. It also shows the correct state for all entities on my first page, but ‘unavailable’ when I have just the single instance. Hence, why I figured that was where the problem was occurring.

Thanks for the feedback.

Well, I think there were a couple of problems in that section of code. One was that I was looking at this._state and not this_state.state, which is what returns ‘on’ or ‘off’. Somehow it was working on the page full of overlays but not on a single call.
I modified the line to:

const stateStr = await this.getState();

and then added the following function:

getState() {
        return new Promise((resolve) => {
            setTimeout(() => {
                const stateStr = this._state.state;
                resolve(stateStr);
            }, 10);
        });
    }

This assures that the state is loaded. Although it does seem to slow things down a bit and there’s a noticable delay when changing the color. Here’s my entire code if anyone is interested.

// custom: image-hue-rotate-card
// created by Art Whitehead

const CARD_VERSION = '1.0.0';
console.info(`%c  IMAGE-HUE-ROTATE-CARD  \n%c  Version ${CARD_VERSION}          `, 'color: orange; font-weight: bold; background: black', 'color: white; font-weight: bold; background: dimgray');

class ImageHueRotateCard extends HTMLElement {

    //private properties
    _config;
    _hass;
    _elements = {};
    _state;
    _imWidth;
    _imHeight;

    constructor() {
        super();
        this.createCard();
        this.createStyle();
        this.attachCard();
        this.setElements();   
    }

    setConfig(config) {
        if (!config.entity) {
            throw new Error("You need to define an entity");
        }
        if (!config.state_image || !config.state_image["on"]) {
            throw new Error("You need to define an 'ON' state image");
        } 
        if (!config.state_image["off"]) {
            throw new Error("You need to define an 'OFF' state image");
        }
        if (!config.state_image["unavailable"]) {
            throw new Error("You need to define an unavailable state image");
        }

        this._config = config;
    }

    set hass(hass) {
        this._hass = hass;
        this.updateHass();
    }

    onClicked() {
        this.doToggle();
    }

    connectedCallback() {
        this.getCanvasInfo();
    }

    createCard() {
        this._elements.card = document.createElement('ha-card');
        this._elements.card.innerHTML = `
            <div class="ihr-card">
                <canvas class="c1" id ="canvas1" style="display: block">
                </canvas>
            </div>
        `;
    }

    createStyle() {
        this._elements.style = document.createElement('style');
        this._elements.style.textContent = `
            .type-custom-image-canvas-card {
                padding: 0 0 0 0;
                margin: 0 0 0 0;
                overflow: hidden;
            }
            .ihr-card {
                padding: 0 0 0 0;
                margin: 0 0 0 0;
                overflow: hidden;
            }
        `
    }
    attachCard(){
        this.attachShadow({mode: 'open'});
        this.shadowRoot.append(this._elements.style, this._elements.card);
    }

    setElements() {
        const card = this._elements.card;
        this._elements.dimSrc = card.querySelector(".ihr-card");
        this._elements.dimSrc.addEventListener("click", this.onClicked.bind(this), false);
    }

    updateHass() {
        const entityID = this._config.entity;
        this._state = this._hass.states[entityID];
    }

    getEntityID() {
        return this._config.entity;
    }
    
    doToggle() {
        this._hass.callService('light', 'toggle', {
            entity_id: this.getEntityID()
        });
    }

    getState() {
        return new Promise((resolve) => {
            setTimeout(() => {
                const stateStr = this._state.state;
                resolve(stateStr);
            }, 10);
        });
    }

    getImageWidth(src) {
		return new Promise((resolve) => {
			const img = new Image();
			img.onload = () => resolve (img.width);
			img.src = src;
		});
	}

	getImageHeight(src) {
		return new Promise((resolve) => {
			const img = new Image();
			img.onload = () => resolve (img.height);
			img.src = src;
		});
	}

    async getCanvasInfo() {
        const stateStr = await this.getState();

        if (stateStr == 'on') {
            var imageSRC = this._config.state_image["on"];
        } else if (stateStr == 'off') {
            var imageSRC = this._config.state_image["off"];
        } else {
            var imageSRC = this._config.state_image["unavailable"];
        }

        this._imWidth = await this.getImageWidth(imageSRC);
		this._imHeight = await this.getImageHeight(imageSRC);
	            
        const cardSource = this._elements.dimSrc
        const cardSize = cardSource.getBoundingClientRect();
        
        var w = cardSize.width;
        var h = this._imHeight * w / this._imWidth;

        this.drawCanvas(imageSRC, w, h, stateStr, cardSource);
		
    }
    drawCanvas(src, w, h, state, card){
        const canvas = this._elements.card.querySelector('.c1');
	    const ctx = canvas.getContext('2d',{willReadFrequently: true});
        
        const image1 = new Image();
        image1.src = src;

        ctx.canvas.width = w;
		ctx.canvas.height = h;
		ctx.drawImage(image1, 0, 0, w, h);

        if(state == 'on'){
            this.changeHue(ctx, w, h);
        }

        setInterval(() => {
            let currentCardSize = card.getBoundingClientRect();
            if (currentCardSize.width != w) {
                this.reDrawCanvas(ctx, currentCardSize.width, w, h, image1, state);
                w = currentCardSize.width;
                h = this._imHeight * w / this._imWidth;;
            }           
        })
    }

    reDrawCanvas(ctx, newW, w, h, img, state) {
		var scale = newW / w;
        var newH = h * scale;
		ctx.canvas.width = newW;
		ctx.canvas.height = newH;
		ctx.scale(scale, scale);
		ctx.drawImage(img, 0, 0, w, h);
        if(state == 'on'){
            this.changeHue(ctx, w, h);
        }
		ctx.scale(-scale, -scale);
	}

    changeHue(ctx, w, h) {
        const entityID = this._config.entity;
		const colorHue  = this._hass.states[entityID].attributes['hs_color'][0];
		var colorSat    = this._hass.states[entityID].attributes['hs_color'][1];
		const targetR   = this._hass.states[entityID].attributes['rgb_color'][0];
		const targetG   = this._hass.states[entityID].attributes['rgb_color'][1];
		const targetB   = this._hass.states[entityID].attributes['rgb_color'][2];
		const colorMode = this._hass.states[entityID].attributes['color_mode'];
        const colorTemp = this._hass.states[entityID].attributes['color_temp_kelvin'];
        const colorLum = this.getLuminence(targetR, targetG, targetB);
        					
		if (colorMode == 'color_temp') {
		    colorSat /= (6500 /colorTemp);
		}

        const targetHSL = new Array(colorHue, colorSat, colorLum);
					
        const imgData = ctx.getImageData(0, 0, w, h);
			for (let i = 0; i < imgData.data.length; i += 4) {
        		this.shiftHue(i, imgData, targetHSL);
			}
			ctx.putImageData(imgData, 0, 0);
    }

    shiftHue(i, imgData, hsl) {
        var red = imgData.data[i] / 255;
        var green = imgData.data[i + 1] / 255;
        var blue = imgData.data[i + 2] / 255;
        var alpha = imgData.data[i + 3];
    
        //if alpha is 0, no sense calculating the shift
        if (alpha == 0) {
            return imgData; 
        }

        var hue = hsl[0] / 360;
        var satShift = hsl[1] / 100;
        var lumShift = hsl[2];
        
        var minColor = Math.min(Math.min(red, green), blue);
        var maxColor = Math.max(Math.max(red, green), blue);
        	
        var luminence = ((minColor + maxColor) / 2) * lumShift;
        
        if (minColor == maxColor) {
            var saturation = 0;
        } else if (luminence <= 0.5) {
            var saturation = ((maxColor - minColor) / (maxColor + minColor)) * satShift;
        } else {
            var saturation = ((maxColor - minColor) / (2.0 - maxColor - minColor)) * satShift;
        }
        
        if (luminence < 0.5) {
            var temp1 = luminence * (1 + saturation);
        } else {
            var temp1 = (luminence + saturation) - (luminence * saturation)
        }
        var temp2 = (2 * luminence) - temp1;
        
        if ((hue + (1/3)) > 1) {
            var tempR = hue + (1/3) - 1;
        } else {
            var tempR = hue + (1/3);
        }						
        var tempG = hue;
        if ((hue - (1/3)) < 0) {
            var tempB = 1 + hue - (1/3);
        } else {
            var tempB = hue - (1/3);
        }
        imgData.data[i]     = this.getNewColorValue(tempR, hue, temp1, temp2);
        imgData.data[i + 1] = this.getNewColorValue(tempG, hue, temp1, temp2);
        imgData.data[i + 2] = this.getNewColorValue(tempB, hue, temp1, temp2);
        imgData.data[i + 3] = alpha;
        return imgData;
    }
    
    getLuminence(r, g, b) {
            r = r / 255;
            g = g / 255;
            b = b / 255;
        
            var minColor = Math.min(Math.min(r, g), b);
            var maxColor = Math.max(Math.max(r, g), b);
            
            var luminence = (minColor + maxColor) / 2;
            
            return luminence;
    }

    getNewColorValue(color, hue, temp1, temp2) {
        if((6 * color) < 1) {
            return Math.round((temp2 + (temp1 - temp2) * 6 * color) * 255);
        } else if ((2 * color) < 1) {
            return Math.round(temp1 * 255);
        } else if ((3 * color) < 2) {
            return Math.round((temp2 + (temp1 - temp2) * ((2/3) - color) * 6) * 255);
        } else {
            return Math.round(temp2 * 255);
        }
    }
}

customElements.define('image-hue-rotate-card', ImageHueRotateCard);

window.customCards = window.customCards || [];
window.customCards.push({
    type: "image-hue-rotate-card",
    name: "Image Hue Rotate Card",
    description: "An improvement to using CSS filter hue-rotate"
});