5180 fichiers 38.5 Mo 0 téléchargements 352 vues mis à jour il y a 7 heures ui
nitro-v36-with-renderer-v36 / src / components / camera / views / editor / CameraWidgetEditorView.tsx
CameraWidgetEditorView.tsx 15.3 Ko Télécharger le fichier
import {
    CreateLinkEvent,
    GetRoomCameraWidgetManager,
    IRoomCameraWidgetEffect,
    IRoomCameraWidgetSelectedEffect,
    OctaneLogger,
    RoomCameraWidgetSelectedEffect
} from '@octane/renderer';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { CameraEditorTabs, CameraPicture, CameraPictureThumbnail, LocalizeText } from '../../../../api';
import { OctaneCardView, Slider } from '../../../../common';
import { TabBar, TabBarTab } from '../../../../common/TabBar';
import { CAMERA_BOX_COLOR, CameraCenteredText, CameraNativeText } from '../CameraNativeText';
import { CameraSkinButton } from '../CameraSkinButton';
import { CameraWidgetEffectListView } from './effect-list';
import { BaseButton } from '../../../../common/BaseButton';
import { ToggleButton } from '../../../../common/ToggleButton';
import { NativeTextTree } from '../../../../common/native-text/NativeTextTree';

export interface CameraWidgetEditorViewProps {
    picture: CameraPicture;
    availableEffects: IRoomCameraWidgetEffect[];
    myLevel: number;
    onClose: () => void;
    onCancel: () => void;
    // The edited preview, plus what the AIR scene needs when the server renders: the effects that are on and the zoom (1 or 2).
    onCheckout: (pictureUrl: string, effects: IRoomCameraWidgetSelectedEffect[], zoom: number) => void;
}

const TABS: string[] = [CameraEditorTabs.COLORMATRIX, CameraEditorTabs.COMPOSITE];
const DEFAULT_EFFECT_STRENGTH: number = 0.5;
const EFFECT_RENDER_DEBOUNCE: number = 50;

const getDownloadName = (): string => {
    const now = new Date();
    const pad = (value: number) => value.toString().padStart(2, '0');

    return `Habbo_${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}.png`;
};

export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = (props) => {
    const { picture = null, availableEffects = [], myLevel = 1, onClose = null, onCancel = null, onCheckout = null } = props;
    const [currentTab, setCurrentTab] = useState(TABS[0]);
    const [selectedEffectName, setSelectedEffectName] = useState<string>(null);
    const [selectedEffects, setSelectedEffects] = useState<IRoomCameraWidgetSelectedEffect[]>([]);
    const [effectsThumbnails, setEffectsThumbnails] = useState<CameraPictureThumbnail[]>([]);
    const [isZoomed, setIsZoomed] = useState(false);
    const [isRendering, setIsRendering] = useState(false);
    const [currentPictureUrl, setCurrentPictureUrl] = useState<string>(picture?.imageUrl ?? '');
    const requestIdRef = useRef<number>(0);
    const stableTexture = picture?.texture ?? null;

    const colorMatrixEffects = useMemo(() => availableEffects.filter((effect) => effect.colorMatrix), [availableEffects]);
    const compositeEffects = useMemo(() => availableEffects.filter((effect) => effect.texture), [availableEffects]);
    const visibleEffects = currentTab === CameraEditorTabs.COLORMATRIX ? colorMatrixEffects : compositeEffects;

    const getSelectedEffectIndex = useCallback(
        (name: string) => {
            if (!name || !selectedEffects.length) return -1;

            return selectedEffects.findIndex((effect) => effect.effect.name === name);
        },
        [selectedEffects]
    );

    const currentEffectIndex = getSelectedEffectIndex(selectedEffectName);
    const currentEffect = currentEffectIndex >= 0 ? selectedEffects[currentEffectIndex] : null;

    const setSelectedEffectAlpha = useCallback(
        (alpha: number) => {
            if (currentEffectIndex < 0) return;

            setIsRendering(true);
            setSelectedEffects((previous) => {
                const next = [...previous];
                const selectedEffect = next[currentEffectIndex];

                next[currentEffectIndex] = new RoomCameraWidgetSelectedEffect(selectedEffect.effect, alpha);

                return next;
            });
        },
        [currentEffectIndex]
    );

    const processAction = useCallback(
        (type: string, effectName: string = null) => {
            switch (type) {
                case 'close':
                    onClose();
                    return;
                case 'cancel':
                    onCancel();
                    return;
                case 'checkout':
                    if (!isRendering && currentPictureUrl) onCheckout(currentPictureUrl, selectedEffects, isZoomed ? 2 : 1);
                    return;
                case 'change_tab':
                    setCurrentTab(String(effectName));
                    setSelectedEffectName(null);
                    return;
                case 'select_effect': {
                    const effect = availableEffects.find((availableEffect) => availableEffect.name === effectName);

                    if (!effect || effect.minLevel > myLevel) return;

                    setSelectedEffectName(effect.name);

                    if (getSelectedEffectIndex(effectName) >= 0) return;

                    setIsRendering(true);
                    setSelectedEffects((previous) => {
                        const effectsWithoutAnotherFrame =
                            effect.type === 'frame' ? previous.filter((selectedEffect) => selectedEffect.effect.type !== 'frame') : previous;

                        return [
                            ...effectsWithoutAnotherFrame,
                            new RoomCameraWidgetSelectedEffect(effect, effect.type === 'frame' ? 1 : DEFAULT_EFFECT_STRENGTH)
                        ];
                    });
                    return;
                }
                case 'remove_effect': {
                    const existingIndex = getSelectedEffectIndex(effectName);

                    if (existingIndex < 0) return;

                    setIsRendering(true);
                    setSelectedEffects((previous) => previous.filter((_, index) => index !== existingIndex));

                    if (selectedEffectName === effectName) setSelectedEffectName(null);
                    return;
                }
                case 'download': {
                    if (!currentPictureUrl) return;

                    const link = document.createElement('a');

                    link.href = currentPictureUrl;
                    link.download = getDownloadName();
                    link.rel = 'noopener';
                    link.click();
                    return;
                }
                case 'zoom':
                    setIsRendering(true);
                    setIsZoomed((previous) => !previous);
                    return;
            }
        },
        [
            availableEffects,
            currentPictureUrl,
            getSelectedEffectIndex,
            isRendering,
            myLevel,
            onCancel,
            onCheckout,
            onClose,
            isZoomed,
            selectedEffectName,
            selectedEffects
        ]
    );

    // useCamera loads the effects once the hotel is idle; opening the editor first loads them now.
    useEffect(() => {
        const manager = GetRoomCameraWidgetManager();

        if (!manager.isLoaded) void manager.init();
    }, []);

    useEffect(() => {
        if (!stableTexture) return;

        let cancelled = false;

        const processThumbnails = async () => {
            for (const effect of availableEffects) {
                if (effect.minLevel > myLevel) continue;

                try {
                    const image = await GetRoomCameraWidgetManager().applyEffects(stableTexture, [new RoomCameraWidgetSelectedEffect(effect, 1)], false);

                    if (cancelled) return;

                    setEffectsThumbnails((previous) => [
                        ...previous.filter((thumbnail) => thumbnail.effectName !== effect.name),
                        new CameraPictureThumbnail(effect.name, image.src)
                    ]);
                } catch (error) {
                    OctaneLogger.error(`Failed to render camera effect thumbnail ${effect.name}`, error);
                }
            }
        };

        void processThumbnails();

        return () => {
            cancelled = true;
        };
    }, [stableTexture, availableEffects, myLevel]);

    useEffect(() => {
        if (!stableTexture) return;

        const requestId = ++requestIdRef.current;

        const debounceTimer = setTimeout(() => {
            if (!selectedEffects.length && !isZoomed) {
                if (requestId === requestIdRef.current) {
                    setCurrentPictureUrl(picture.imageUrl);
                    setIsRendering(false);
                }

                return;
            }

            GetRoomCameraWidgetManager()
                .applyEffects(stableTexture, selectedEffects, isZoomed)
                .then((imageElement) => {
                    if (requestId !== requestIdRef.current) return;

                    setCurrentPictureUrl(imageElement.src);
                    setIsRendering(false);
                })
                .catch((error) => {
                    if (requestId === requestIdRef.current) {
                        setCurrentPictureUrl('');
                        setIsRendering(false);
                    }

                    OctaneLogger.error('Failed to apply effects to picture', error);
                });
        }, EFFECT_RENDER_DEBOUNCE);

        return () => clearTimeout(debounceTimer);
    }, [stableTexture, selectedEffects, isZoomed, picture]);

    useEffect(() => {
        return () => {
            requestIdRef.current++;
        };
    }, []);

    return (
        <NativeTextTree>
            <OctaneCardView className="octane-camera-editor" frameStyle={3} isResizable={false} style={{ resize: 'none' }}>
                <div className="octane-card-header-shell">
                    <span className="octane-card-title">
                        <CameraCenteredText
                            background={0x484949}
                            color={0xffffff}
                            text={LocalizeText('camera.editor.button.text')}
                            textStyle="u_frame_title"
                            width={584}
                        />
                    </span>
                    <BaseButton aria-label={LocalizeText('generic.help')} className="octane-camera-editor__help" type="button" onClick={() => CreateLinkEvent('habbopages/camera')} />
                    <BaseButton aria-label={LocalizeText('generic.close')} className="octane-card-close-button" type="button" onClick={() => processAction('close')} />
                </div>
                <div className="octane-camera-editor__layout">
                    <TabBar className="octane-camera-editor__effect-tabs" value={currentTab}>
                        {TABS.map((tab) => (
                            <TabBarTab
                                key={tab}
                                value={tab}
                                className={`octane-camera-editor__effect-tab${currentTab === tab ? ' octane-camera-editor__effect-tab--active' : ''}`}
                                title={LocalizeText(`camera.effect.category.${tab}`)}
                                onClick={() => processAction('change_tab', tab)}
                            >
                                <i className={`octane-icon icon-camera-${tab}`} />
                            </TabBarTab>
                        ))}
                    </TabBar>

                    <div className="octane-camera-editor__effect-grid-frame has-classic-scrollbar">
                        <CameraWidgetEffectListView
                            myLevel={myLevel}
                            selectedEffectName={selectedEffectName}
                            selectedEffects={selectedEffects}
                            effects={visibleEffects}
                            thumbnails={effectsThumbnails}
                            processAction={processAction}
                        />
                    </div>

                    <div className="octane-camera-editor__preview" onClick={() => setSelectedEffectName(null)}>
                        {currentPictureUrl && <img alt="" src={currentPictureUrl} />}
                    </div>

                    {currentEffect && currentEffect.effect.type !== 'frame' && (
                        <div className="octane-camera-editor__slider-panel">
                            <div className="octane-camera-editor__slider-label">
                                <CameraCenteredText
                                    background={0x000000}
                                    color={0xeeeeee}
                                    style={{ mixBlendMode: 'screen' }}
                                    text={`${LocalizeText(`camera.effect.name.${currentEffect.effect.name}`)} ${Math.round(currentEffect.strength * 100)}%`}
                                    textStyle="u_bold"
                                    width={320}
                                />
                            </div>
                            <Slider
                                disabledButton
                                className="octane-camera-editor__slider"
                                min={0}
                                max={100}
                                step={1}
                                value={Math.round(currentEffect.strength * 100)}
                                onChange={(value) => setSelectedEffectAlpha(value / 100)}
                                thumbAriaLabel={LocalizeText(`camera.effect.name.${currentEffect.effect.name}`)}
                            />
                        </div>
                    )}

                    <BaseButton type="button" className="octane-camera-editor__tool octane-camera-editor__tool--save" onClick={() => processAction('download')}>
                        <span className="octane-camera-editor__tool-icon" aria-hidden="true" />
                        <CameraNativeText background={CAMERA_BOX_COLOR} color={0x000000} size={11} text={LocalizeText('floor.plan.editor.save')} textStyle="u_button_tab" underline />
                    </BaseButton>
                    <ToggleButton
                        type="button"
                        className="octane-camera-editor__tool octane-camera-editor__tool--zoom"
                        pressed={isZoomed}
                        onClick={() => processAction('zoom')}
                    >
                        <span className="octane-camera-editor__tool-icon" aria-hidden="true" />
                        <CameraNativeText background={CAMERA_BOX_COLOR} color={0x000000} size={11} text={LocalizeText('room.zoom.button.text')} textStyle="u_button_tab" underline />
                    </ToggleButton>

                    <div className="octane-camera-editor__button-separator" />
                    <CameraSkinButton className="octane-camera-editor__cancel" label={LocalizeText('catalog.purchase_confirmation.cancel')} labelWidth={152} variant="gray-dark" onClick={() => processAction('cancel')} />
                    <CameraSkinButton
                        className="octane-camera-editor__purchase"
                        disabled={isRendering || !currentPictureUrl}
                        label={LocalizeText('camera.preview.button.text')}
                        labelWidth={152}
                        variant="thick-green"
                        onClick={() => processAction('checkout')}
                    />
                </div>
            </OctaneCardView>
        </NativeTextTree>
    );
};
15Sujets
1Dépôts
20Membres
3En 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