/ src / components / avatar.jsx
avatar.jsx
 1  import './avatar.css';
 2  
 3  import { useRef } from 'preact/hooks';
 4  
 5  import mem from '../utils/mem';
 6  
 7  const SIZES = {
 8    s: 16,
 9    m: 20,
10    l: 24,
11    xl: 40,
12    xxl: 50,
13    xxxl: 64,
14  };
15  
16  const alphaCache = {};
17  
18  const canvas = window.OffscreenCanvas
19    ? new OffscreenCanvas(1, 1)
20    : document.createElement('canvas');
21  const ctx = canvas.getContext('2d', {
22    willReadFrequently: true,
23  });
24  
25  function Avatar({ url, size, alt = '', squircle, ...props }) {
26    size = SIZES[size] || size || SIZES.m;
27    const avatarRef = useRef();
28    const isMissing = /missing\.png$/.test(url);
29    return (
30      <span
31        ref={avatarRef}
32        class={`avatar ${squircle ? 'squircle' : ''} ${
33          alphaCache[url] ? 'has-alpha' : ''
34        }`}
35        style={{
36          width: size,
37          height: size,
38        }}
39        title={alt}
40        {...props}
41      >
42        {!!url && (
43          <img
44            src={url}
45            width={size}
46            height={size}
47            alt={alt}
48            loading="lazy"
49            decoding="async"
50            crossOrigin={
51              alphaCache[url] === undefined && !isMissing
52                ? 'anonymous'
53                : undefined
54            }
55            onError={(e) => {
56              if (e.target.crossOrigin) {
57                e.target.crossOrigin = null;
58                e.target.src = url;
59              }
60            }}
61            onLoad={(e) => {
62              if (avatarRef.current) avatarRef.current.dataset.loaded = true;
63              if (alphaCache[url] !== undefined) return;
64              if (isMissing) return;
65              try {
66                // Check if image has alpha channel
67                const { width, height } = e.target;
68                if (canvas.width !== width) canvas.width = width;
69                if (canvas.height !== height) canvas.height = height;
70                ctx.drawImage(e.target, 0, 0);
71                const allPixels = ctx.getImageData(0, 0, width, height);
72                // At least 10% of pixels have alpha <= 128
73                const hasAlpha =
74                  allPixels.data.filter((pixel, i) => i % 4 === 3 && pixel <= 128)
75                    .length /
76                    (allPixels.data.length / 4) >
77                  0.1;
78                if (hasAlpha) {
79                  // console.log('hasAlpha', hasAlpha, allPixels.data);
80                  avatarRef.current.classList.add('has-alpha');
81                }
82                alphaCache[url] = hasAlpha;
83                ctx.clearRect(0, 0, width, height);
84              } catch (e) {
85                // Silent fail
86                alphaCache[url] = false;
87              }
88            }}
89          />
90        )}
91      </span>
92    );
93  }
94  
95  export default mem(Avatar);