5180 fichiers 38.5 Mo 0 téléchargements 262 vues mis à jour il y a 5 heures ui
nitro-v36-with-renderer-v36 / src / common / layout / LayoutBadgeImageView.tsx
LayoutBadgeImageView.tsx 11.8 Ko Télécharger le fichier
import { BadgeImageReadyEvent, GetEventDispatcher, GetSessionDataManager, OctaneSprite, TextureUtils } from '@octane/renderer';
import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react';
import {
    BadgeLeaderboardStat,
    ensureBadgeLeaderboardLoaded,
    GetConfigurationValue,
    getCachedBadgeRarityStat,
    LocalizeBadgeDescription,
    LocalizeBadgeName,
    LocalizeText
} from '../../api';
import { getBadgeRarityFromPacket, getBadgeRarityGlowColor, getBadgeRarityTier, isBadgeRarityStandaloneTier } from '../../api/badges/badgeRarity';
import { Base, BaseProps } from '../Base';
import { BadgeDetailsTooltip } from './BadgeDetailsTooltip';
import { PIXEL_ART_RENDERING } from './PixelArtRendering';

export interface LayoutBadgeImageViewProps extends BaseProps<HTMLDivElement> {
    badgeCode: string;
    isGroup?: boolean;
    /** A group badge being edited: always drawn by the client, never asked from the badge imager. */
    isPreview?: boolean;
    showInfo?: boolean;
    customTitle?: string;
    isGrayscale?: boolean;
    scale?: number;
    showRarityInfo?: boolean;
    highlightRarity?: boolean;
}

/* Glow on the badge itself (highlightRarity), after the official BadgeImageWidget.playGlow:
   nothing is drawn at rest; when the badge is set and again on mouse-over a 500ms run
   raises the glow strength from 0 to 1 with an ease-in-out cubic and then clears it. The
   filters are an outer glow, an inner glow and a tint towards the tier colour. Only
   standalone tiers glow - rare and above, plus uncommon when `badge_rarity.uncommon` is on. */
const BADGE_GLOW_DURATION_MS = 500;

const glowCss = (color: number): string => {
    const red = (color >> 16) & 0xff;
    const green = (color >> 8) & 0xff;
    const blue = color & 0xff;

    return `rgb(${red}, ${green}, ${blue})`;
};

export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = (props) => {
    const {
        badgeCode = null,
        isGroup = false,
        isPreview = false,
        showInfo = false,
        customTitle = null,
        isGrayscale = false,
        scale = 1,
        showRarityInfo = false,
        highlightRarity = false,
        classNames = [],
        style = {},
        children = null,
        ...rest
    } = props;
    const [imageElement, setImageElement] = useState<HTMLImageElement>(null);
    const [badgeRarityStat, setBadgeRarityStat] = useState<BadgeLeaderboardStat>(null);
    // The badges packet (1087) carries the official tier per worn slot; it
    // wins over the leaderboard classification when the server sent it.
    const packetRarity = !isGroup && badgeCode && (showRarityInfo || highlightRarity) ? getBadgeRarityFromPacket(badgeCode) : null;
    const badgeRef = useRef<HTMLDivElement>(null);
    const [glowRun, setGlowRun] = useState(0);
    const [glowBreathing, setGlowBreathing] = useState(false);

    const tooltipsEnabled = showInfo && GetConfigurationValue<boolean>('badge.descriptions.enabled', true);
    const uncommonRarityEnabled = GetConfigurationValue<boolean>('badge_rarity.uncommon', false) === true;
    const rarityTier = packetRarity ? packetRarity.tier : badgeRarityStat ? getBadgeRarityTier(badgeRarityStat.rarity) : null;
    const ownerCount = packetRarity ? packetRarity.ownerCount : badgeRarityStat ? badgeRarityStat.ownerCount : 0;

    const getClassNames = useMemo(() => {
        const newClassNames: string[] = ['relative w-[40px] h-[40px] bg-no-repeat bg-center'];

        if (isGroup) newClassNames.push('group-badge');

        if (isGrayscale) newClassNames.push('grayscale');

        if (classNames.length) newClassNames.push(...classNames);

        return newClassNames;
    }, [classNames, isGroup, isGrayscale]);

    const getStyle = useMemo(() => {
        let newStyle: CSSProperties = {};

        if (imageElement) {
            newStyle.backgroundImage = `url(${isGroup ? imageElement.src : GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode.toString())})`;
            newStyle.width = imageElement.width;
            newStyle.height = imageElement.height;

            if (scale !== 1) {
                newStyle.transform = `scale(${scale})`;

                if (!(scale % 1)) newStyle.imageRendering = PIXEL_ART_RENDERING;

                newStyle.width = imageElement.width * scale;
                newStyle.height = imageElement.height * scale;
            }
        }

        if (highlightRarity && rarityTier !== null && isBadgeRarityStandaloneTier(rarityTier, uncommonRarityEnabled)) {
            (newStyle as Record<string, string>)['--badge-glow'] = glowCss(getBadgeRarityGlowColor(rarityTier, uncommonRarityEnabled));
        }

        if (Object.keys(style).length) newStyle = { ...newStyle, ...style };

        return newStyle;
    }, [badgeCode, rarityTier, highlightRarity, isGroup, imageElement, scale, style, uncommonRarityEnabled]);

    useEffect(() => {
        if (!badgeCode || !badgeCode.length) return;

        let didSetBadge = false;
        let cancelled = false;
        // Only the newest image counts: the loading icon read back from the GPU can finish after the real badge.
        let latestRequest = 0;
        const allowServerImage = !isPreview;

        const showImage = (element: HTMLImageElement, request: number) => {
            const apply = () => {
                if (!cancelled && request === latestRequest) setImageElement(element);
            };

            if (element.complete && element.naturalWidth) apply();
            else element.onload = apply;
        };

        // A group badge has one image URL, made once by the renderer and shared by every view. Only a renderer
        // without it (or a badge it could not draw on a canvas) still reads the texture back from the GPU.
        const showGroupBadge = async (texture: BadgeImageReadyEvent['image']) => {
            const request = ++latestRequest;
            const url = GetSessionDataManager().getGroupBadgeImageUrl?.(badgeCode, allowServerImage);

            if (url) {
                const element = new Image();

                element.src = url;
                showImage(element, request);

                return;
            }

            if (!texture) return;

            const sprite = new OctaneSprite(texture);
            const element = await TextureUtils.generateImage({ target: sprite, resolution: 1 });

            // Drop the wrapper only; the badge texture stays with the session data manager.
            sprite.destroy();

            if (element) showImage(element, request);
        };

        const onBadgeImageReadyEvent = async (event: BadgeImageReadyEvent) => {
            if (event.badgeId !== badgeCode) return;

            if (isGroup) {
                await showGroupBadge(event.image);
            } else {
                const badgeUrl = GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode.toString());
                const img = new Image();

                img.onload = () => setImageElement(img);
                img.src = badgeUrl;
            }

            didSetBadge = true;

            GetEventDispatcher().removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
        };

        GetEventDispatcher().addEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);

        const texture = isGroup ? GetSessionDataManager().getGroupBadgeImage(badgeCode, allowServerImage) : GetSessionDataManager().getBadgeImage(badgeCode);

        if (texture && !didSetBadge) {
            if (isGroup) {
                void showGroupBadge(texture);
            } else {
                const badgeUrl = GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode.toString());
                const img = new Image();

                img.onload = () => setImageElement(img);
                img.src = badgeUrl;
            }
        }

        return () => {
            cancelled = true;
            GetEventDispatcher().removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
        };
    }, [badgeCode, isGroup, isPreview]);

    useEffect(() => {
        if (isGroup || !badgeCode || (!showRarityInfo && !highlightRarity) || getBadgeRarityFromPacket(badgeCode)) {
            setBadgeRarityStat(null);
            return;
        }

        const cached = getCachedBadgeRarityStat(badgeCode);

        if (cached) {
            setBadgeRarityStat(cached);
            return;
        }

        let cancelled = false;

        ensureBadgeLeaderboardLoaded()
            .then(() => {
                if (cancelled) return;

                setBadgeRarityStat(getCachedBadgeRarityStat(badgeCode));
            })
            .catch(() => {
                if (cancelled) return;

                setBadgeRarityStat(null);
            });

        return () => {
            cancelled = true;
        };
    }, [badgeCode, highlightRarity, isGroup, showRarityInfo]);

    const canGlow = highlightRarity && rarityTier !== null && isBadgeRarityStandaloneTier(rarityTier, uncommonRarityEnabled);

    // Official ExtendedProfileWindowCtrl.setSelectedBadge / showBadgeInfo: play the glow once
    // the badge is set, and again on every mouse-over.
    useEffect(() => {
        if (canGlow && imageElement) setGlowRun((run) => run + 1);
    }, [canGlow, imageElement]);

    // While the pointer rests on the badge the run keeps breathing: the same curve, played
    // back and forth, instead of the official single run.
    const startBreathing = () => {
        if (canGlow) setGlowBreathing(true);
    };

    const stopBreathing = () => setGlowBreathing(false);

    const [isTooltipVisible, setIsTooltipVisible] = useState(false);
    const glowAnimation: CSSProperties = glowBreathing
        ? { animationDuration: `${BADGE_GLOW_DURATION_MS * 2}ms`, animationIterationCount: 'infinite', animationDirection: 'alternate' }
        : { animationDuration: `${BADGE_GLOW_DURATION_MS}ms` };

    return (
        <Base
            innerRef={badgeRef}
            classNames={getClassNames}
            style={getStyle}
            {...rest}
            onMouseEnter={() => {
                if (tooltipsEnabled) setIsTooltipVisible(true);
                startBreathing();
            }}
            onMouseLeave={() => {
                setIsTooltipVisible(false);
                stopBreathing();
            }}
        >
            {canGlow && (glowRun > 0 || glowBreathing) && (
                <span key={glowBreathing ? 'breathing' : glowRun} className="badge-glow" aria-hidden="true">
                    {/* Breathing touches only the outline: the badge pixels stay as they are. */}
                    <span
                        className={`badge-glow-run${glowBreathing ? ' badge-glow-run--outline' : ''}`}
                        style={{ ...glowAnimation, backgroundImage: getStyle.backgroundImage as string }}
                    />
                    {!glowBreathing && (
                        <span
                            className="badge-glow-tint"
                            style={{
                                ...glowAnimation,
                                maskImage: getStyle.backgroundImage as string,
                                WebkitMaskImage: getStyle.backgroundImage as string
                            }}
                        />
                    )}
                </span>
            )}
            {tooltipsEnabled && isTooltipVisible && (
                <BadgeDetailsTooltip
                    key={badgeCode}
                    anchor={badgeRef}
                    name={isGroup ? (customTitle ?? '') : LocalizeBadgeName(badgeCode)}
                    description={isGroup ? LocalizeText('group.badgepopup.body') : LocalizeBadgeDescription(badgeCode)}
                    rarityTier={showRarityInfo ? rarityTier : null}
                    ownerCount={ownerCount}
                    uncommonEnabled={uncommonRarityEnabled}
                />
            )}
            {children}
        </Base>
    );
};
15Sujets
1Dépôts
20Membres
2En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser