Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import { observer } from "mobx-react";
import { forwardRef, useCallback, useMemo } from "react";
import { cn } from "../../utils/bem";
import messages from "../../utils/messages";
import { ErrorMessage } from "../ErrorMessage/ErrorMessage";
import "./Image.scss";
 
/**
 * Coordinates in relative mode belong to a data domain consisting of percentages in the range from 0 to 100
 */
export const RELATIVE_STAGE_WIDTH = 100;
 
/**
 * Coordinates in relative mode belong to a data domain consisting of percentages in the range from 0 to 100
 */
export const RELATIVE_STAGE_HEIGHT = 100;
 
/**
 * Mode of snapping to pixel
 */
export const SNAP_TO_PIXEL_MODE = {
  EDGE: "edge",
  CENTER: "center",
};
 
export const Image = observer(
  forwardRef(({ imageEntity, imageTransform, updateImageSize, usedValue, size, overlay }, ref) => {
    const imageSize = useMemo(() => {
      return {
        width: size.width === 1 ? "100%" : size.width,
        height: size.height === 1 ? "auto" : size.height,
      };
    }, [size]);
 
    const onLoad = useCallback(
      (event) => {
        updateImageSize(event);
        imageEntity.setImageLoaded(true);
      },
      [updateImageSize, imageEntity],
    );
 
    return (
      <div className={cn("image").toClassName()} style={imageSize}>
        {overlay}
        <ImageProgress
          downloading={imageEntity.downloading}
          progress={imageEntity.progress}
          error={imageEntity.error}
          src={imageEntity.src}
          usedValue={usedValue}
        />
        {imageEntity.downloaded ? (
          <ImageRenderer
            alt="image"
            ref={ref}
            src={imageEntity.currentSrc}
            onLoad={onLoad}
            isLoaded={imageEntity.imageLoaded}
            imageTransform={imageTransform}
          />
        ) : null}
      </div>
    );
  }),
);
 
const ImageProgress = observer(({ downloading, progress, error, src, usedValue }) => {
  return downloading ? (
    <div className={cn("image-progress").toClassName()}>
      <div className={cn("image-progress").elem("message").toClassName()}>Downloading image</div>
      <progress
        className={cn("image-progress").elem("bar").toClassName()}
        value={progress}
        min="0"
        max={1}
        step={0.0001}
      />
    </div>
  ) : error ? (
    <ImageLoadingError src={src} value={usedValue} />
  ) : null;
});
 
const imgDefaultProps = { crossOrigin: "anonymous" };
 
const ImageRenderer = observer(
  forwardRef(({ src, onLoad, imageTransform, isLoaded }, ref) => {
    const imageStyles = useMemo(() => {
      // We can't just skip rendering the image because we need its dimensions to be set
      // so we just hide it with 0x0 dimensions.
      //
      // Real dimension will still be available via `naturalWidth` and `naturalHeight`
      const style = {
        // For now, we can't fully hide it as it is used by the Magic Wand tool, so this will hide it visually, but allow using it on the canvas.
        // It is still possible that there is another way to get the right image data in the tool, so it's a temporary quick fix
        ...imageTransform,
        clip: "rect(1px, 1px, 1px, 1px)",
      };
 
      return {
        ...style,
        maxWidth: "unset",
        visibility: isLoaded ? "visible" : "hidden",
      };
    }, [imageTransform, isLoaded]);
 
    // biome-ignore lint/a11y/noRedundantAlt: The use of this component justifies this alt text
    return <img {...imgDefaultProps} ref={ref} alt="image" src={src} onLoad={onLoad} style={imageStyles} />;
  }),
);
 
const ImageLoadingError = ({ src, value }) => {
  const error = useMemo(() => {
    return messages.ERR_LOADING_HTTP({
      url: src,
      error: "",
      attr: value,
    });
  }, [src]);
 
  return <ErrorMessage error={error} />;
};