konvajs / konva

Konva.js is an HTML5 Canvas JavaScript framework that extends the 2d context by enabling canvas interactivity for desktop and mobile applications.
http://konvajs.org/
Other
11.35k stars 915 forks source link

[feat] Allow disable DD #1322

Closed eXponenta closed 10 months ago

eXponenta commented 2 years ago

Currently DD always attached to document in module load phase, that dissalow force isBrowser = false for some reason (when there are a virtual events):

https://github.com/konvajs/konva/blob/3219008dfa1de8cfdbfeeb444f38264878df033e/src/DragAndDrop.ts#L152

and there are not ways to detach it from Konva without hacks.

Maybe can be expose a method for detaching/attaching back? Like:

DD.attach = () => {
  if (!Konva.isBrowser || DD._attached) return;

  DD._attached = true;

  window.addEventListener('mouseup', DD._endDragBefore, true);
  window.addEventListener('touchend', DD._endDragBefore, true);

  window.addEventListener('mousemove', DD._drag);
  window.addEventListener('touchmove', DD._drag);

  window.addEventListener('mouseup', DD._endDragAfter, false);
  window.addEventListener('touchend', DD._endDragAfter, false);
}

DD.detach = () => {
  if (!Konva.isBrowser || !DD._attached) return;

  DD._attached = false;

  window.removeEventListener('mouseup', DD._endDragBefore, true);
  window.removeEventListener('touchend', DD._endDragBefore, true);

  window.removeEventListener('mousemove', DD._drag);
  window.removeEventListener('touchmove', DD._drag);

  window.removeEventListener('mouseup', DD._endDragAfter, false);
  window.removeEventListener('touchend', DD._endDragAfter, false);
}
lavrton commented 2 years ago

Why do you need it? What is your use with virtual events?

eXponenta commented 2 years ago

We not use a canvas as canvas, we use it as image source and have more that one canvas instances as panels, BUT we have a main canvas for webgl context that capture events for lookup. Syntetic events is proxed to each stage instance. Our current code is:

import Konva from 'konva';
import { RootState } from 'react-ogl';
import { createRef, MutableRefObject } from 'react';

export const PROXED_EVENTS = ['pointermove', 'pointerdown', 'pointerup', 'pointerover', 'pointerout'] as const;

(Konva as any).isBrowser = false;

/**
 * Forward simulated events to canvas scope to specific stage
 */
export const forwardEvents = (event: PointerEvent | WheelEvent, stage: Konva.Stage) => {
    if (!event || !stage) {
        return;
    }

    const { type } = event;
    const syntEvent = {
        type,
        clientX: event.clientX,
        clientY: event.clientY,
        // for wheel
        deltaX: (event as any).deltaX ?? 0,
        deltaY: (event as any).deltaY ?? 0,
    };

    if (type === 'pointerup') {
        Konva.DD._endDragBefore(syntEvent);
    }

    if (type === 'pointermove') {
        Konva.DD._drag(syntEvent);
    }

    // fire as mouse when pointer
    type.includes('pointer') &&
        stage[`_${type}`]?.({
            ...syntEvent,
            type: type.replace('pointer', 'mouse'),
        });

    // fire as pointer
    stage[`_${type}`]?.(syntEvent);

    // fire drag after AFTER up event
    if (type === 'pointerup') {
        Konva.DD._endDragAfter(syntEvent);
    }
};

export const attachEventProxy = (context: EventTarget, stage: Konva.Stage) => {
    const scope = (event) => forwardEvents(event, stage);

    PROXED_EVENTS.forEach((type) => context.addEventListener(type, scope));

    return () => PROXED_EVENTS.forEach((type) => context.removeEventListener(type, scope));
};

/**
 * Path Konva for use in OGL state and XR, because RAF must be from rendere
 * Allow to use OffscrenCanvas wehn possible and disable DD
 */
export const patchKonvaJS = (state: RootState) => {
    if (Konva.Util['@__pathed__']) {
        return;
    }

    let konvaQueue: Array<(time: number) => void> = [];
    /**
     * Use Konva path, raf sould be requiested in valid phase
     */
    Konva.Util.requestAnimFrame = (callback: any) => konvaQueue.push(callback);
    Konva.Util['@__pathed__'] = true;

    const refCallback: MutableRefObject<any> = createRef();

    refCallback.current = (_state, time) => {
        // update konva frames
        if (konvaQueue.length) {
            const q = konvaQueue;
            konvaQueue = [];
            q.forEach((c) => c && c(time));
        }
    };

    // and register
    state.subscribe(refCallback);

    // try to use offscreen canvas
    // eslint-disable-next-line no-restricted-globals
    if ((self as any).OffscreenCanvas) {
        Konva.Util.createCanvasElement = () => {
            // eslint-disable-next-line no-restricted-globals
            const canvas = new (self as any).OffscreenCanvas(1, 1);
            canvas.style = {};

            return canvas;
        };
    }

    // allso detach events
    // becuase it will corrupt state
    {
        const { DD } = Konva;

        window.removeEventListener('mouseup', DD._endDragBefore, true);
        window.removeEventListener('touchend', DD._endDragBefore, true);

        window.removeEventListener('mousemove', DD._drag);
        window.removeEventListener('touchmove', DD._drag);

        window.removeEventListener('mouseup', DD._endDragAfter, false);
        window.removeEventListener('touchend', DD._endDragAfter, false);
    }
};
eXponenta commented 2 years ago

So, like this: each panel is new stage + layer for independent update, and each stage can has draggables. Panelles is curved, placed in 3D space, this is why need to proxy drag events from synthetic events, that created from raycast result on specific plane. 2D plan in FPS 3D space. Where mouse events used for camera movement and conflicts with drag events (because drag listen same context)

image

lavrton commented 2 years ago

Well, I am not sure about that use case. I see this in the code:

// allso detach events // becuase it will corrupt state

What will it corrupt?

Is it possible for you to create a tiny demo that can reproduce the issue? Can you use current API and just manually unsubscribe as you do in the code?

eXponenta commented 2 years ago

Well, I am not sure about that use case. I see this in the code:

// allso detach events // becuase it will corrupt state

What will it corrupt?

Is it possible for you to create a tiny demo that can reproduce the issue? Can you use current API and just manually unsubscribe as you do in the code?

See: https://codesandbox.io/s/blazing-shape-c1eidt?file=/src/index.js

Konva mapped onto cube, but while you not click disable DD then drag and drop will be bugged

lavrton commented 2 years ago

In this demo I can't move the star. Because I can't click on it. Looks like you position calculation is not correct. I adopted demo to create dots on clicks: https://codesandbox.io/s/hopeful-zeh-xcg5rg?file=/src/index.js

So you can detach all DD events from the global by yourself, right?

eXponenta commented 2 years ago

Looks like you position calculation is not correct.

I forgot disable auto pixel ratio and size of Konva Stage had more than should be (i use screen with 1 DPR, thanks that notice this)

So you can detach all DD events from the global by yourself, right?

Yep, of course, but if DD Api will change - all hacks will down.

lavrton commented 2 years ago

I see. So, personally, I am not very interested in this API. The workaround is working just fine. Even when attach/detach methods are added, you are still using private API here: stage[_${type}]?.(syntEvent);.

I have no plans to change DD API or stage._[event] methods names. Just update carefully.

If you REALLY want that as part of the core Konva API, then your initial proposal sounds good to me. Make a Pull Request.

lavrton commented 2 years ago

Actually, it will be good to have an official demo of that use case. Because I remember several users with the same use case (drawing Konva canvas into 3d scene).

@eXponenta can you fix the demo, to make coordinates system work correctly?