Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for multiple render instances #130

Open
RobbinBaauw opened this issue Feb 14, 2020 · 6 comments
Open

Allow for multiple render instances #130

RobbinBaauw opened this issue Feb 14, 2020 · 6 comments

Comments

@RobbinBaauw
Copy link

I have some ideas about the following but I'd like to hear some comments first, before making it an official RFC:

Context

Vue3 has support for a custom renderer, which we have been trying out. This custom renderer uses WebGL to render, using Vue templates / components. This is a great feature, and it was relatively easy to accomplish.

Request

Right now, it is only possible to register 1 renderer for the entire Vue instance. This means you can't use multiple instances of the same renderer (multi canvases for WebGL) and you can't mix multiple renderers (e.g. DOM & WebGL). It would be really nice if this was possible, to for example allow for easy state sharing between the two renderers.

My proposal

I would propose the following changes to be made:

  • Create methods for creating an instance of a renderer & compiler (they are seperate things, so I think they should be registered seperately as well)
  • Add extra fields in the ComponentOptions; the renderer and the compiler the component will use
  • Type the nodeOps and all other required objects to create a renderer & compiler and provide more context about the renderer & compiler instance being used, so make it easier to create a custom renderer & compiler.

Some other remarks:

  • I suggest to only allow the DOM renderer to be a "top level" renderer when using nested components: DOM > some other renderer 1 > some other renderer 2. This is because I would have no clue DOM > some other renderer > DOM, if you have suggestions I'd like to hear them!
  • If the above restriction is put in place, it would nice if it would also be possible to only define the renderer on the root of the custom-rendered-components until some other renderer is found. Meaning, if I would have the following nested component render structure DOM > DOM > GL > GL > yet another renderer, I would only need to annotate the fact that I want to use GL as renderer on the first one of the two usages, if that makes sense.
  • If nothing is stated, DOM is the default renderer

If something is vague or if you have other remarks I'd love to hear them!

@yyx990803
Copy link
Member

Create methods for creating an instance of a renderer & compiler (they are seperate things, so I think they should be registered seperately as well)

Isn't this already viable? createRenderer is exposed and what you get back is technically a renderer "instance" (with two methods: render and createApp). For the compiler it's just a higher-order function wrapping baseCompile from compiler-core.

Add extra fields in the ComponentOptions; the renderer and the compiler the component will use

I think this would create a ton of complexity internally. Why not just create a wrapper component that mounts its own DOM tree using the custom renderer? Sounds cleaner that way.

Type the nodeOps and all other required objects

They are all typed, can you share more details on what is missing?

@RobbinBaauw
Copy link
Author

I think this would create a ton of complexity internally. Why not just create a wrapper component that mounts its own DOM tree using the custom renderer? Sounds cleaner that way.

If I understand this correctly you're referring to creating a proxy compiler (as registerRuntimeCompiler allows only a single instance) & renderer, which determines on a component basis which compiler & renderer to use. This would be possible, but I don't think there is enough context available in the nodeOps to determine which component you are dealing with. I think you need some way to determine the type of component you are trying to compile & render and I'm not sure how you'd do that with the current codebase. Adding something like parentComponent to the createElement nodeOps would fix this, but I assume there are other options for this as well. Do you have suggestions for this?

Isn't this already viable? createRenderer is exposed and what you get back is technically a renderer "instance" (with two methods: render and createApp). For the compiler it's just a higher-order function wrapping baseCompile from compiler-core.

Yes, I was suggesting changing this so you could pass this single instance to the component options, but as adding those component options adds a lot of complexity idea this is not relevant anymore.

They are all typed, can you share more details on what is missing?

In vuejs/core@27913e6#diff-d21cd2abd79b4b986874aa0b370f92b1R9 this last type was added, I hadn't seen that yet, thanks!

@yyx990803
Copy link
Member

What I mean is something like:

import { createRenderer, h, Fragment } from 'vue'

const { render } = createRenderer({ /* your options */ })

const CustomRendererWrapper = {
  render: () => h('div', { class: 'custom-renderer' }),
  mounted() {
    render(h(Fragment, this.$slots.default()), this.$el)
  }
}

@basvanmeurs
Copy link

We eventually managed to get it to work. The updated hook was also needed to propagate updates to the sub-renderer:

import {createRendererForStage} from "./runtime/runtime"

import { h, Fragment } from 'vue'

export const Vugel = {
    props: {
        settings: {type: Object, default: {w: 600, h: 600}}
    },
    render: () => {
        return h('canvas', { class: 'custom-renderer' })
    },
    mounted() {
        const stageOptions = {...(this as any).settings};
        const stage = new lng.Stage({ ...stageOptions, canvas: (this as any).$el });

        const render = createRendererForStage(stage);
        (this as any).$vugelRender = render;

        render(h(Fragment, (this as any).$slots.default()), render.stageRoot)
    },
    updated() {
        const render = (this as any).$vugelRender;
        render(h(Fragment, (this as any).$slots.default()), render.stageRoot)
    }
}

We still have an issue with template compilation. Example of a HTML template that spawns a webgl-rendered component:

<script type="text/x-template" id="demo-template">
  <div :scale="scale"></div>
  <vugel :settings="{w: 900, h: 900}" v-on:click="click">
    <gl :prop="scale"></gl>
  </vugel>
  <vugel :settings="{w: 900, h: 900}" v-on:click="click">
    <gl :prop="scale * 0.5"></gl>
  </vugel>
</script>

Notice that 'gl' is a sub component. It's template consists out of elements for the webgl render tree and looks like this:

<script type="text/x-template" id="gl-template">
  <node :scale="prop">
    <node>
      <rect v-for="(item,index) in images" :x="item.x" :w="item.w" :y="index * 40" :h="20" :color="0xff00ffff">
        <gl-image :x="index * 100"></gl-image>
      </rect>
    </node>
  </node>
</script>

gl-image is a sub component, while all other nodes such as node, rect are native elements in the webgl render tree

We can't add webgl tree nodes as children (slots) of the 'vugel' element directly because the compiler would use the HTML isNativeTag implementation, so it would mistakingly think that these are components. For the Vugel slots, this is not a problem as we can use a wrapping component in practice.

Unfortunately, a single compiler is registered using registerRuntimeCompiler, and used for all templates including our webgl components. Our workaround requires us to register a wrapping compiler and then decide on whether to use the compiler-vugel or compiler-core based on the properties of the template:

import {
  compile,
  registerRuntimeCompiler,
  RenderFunction
} from 'vue'
import { CompilerOptions } from '@vue/compiler-core'

import compileVugel from "./compiler/compileToFunction"

// Overwrite Vue compiler with our wrapped compiler.
function compileToFunction(
  template: string | HTMLElement,
  options?: CompilerOptions
): RenderFunction {
  if (typeof template === "string") {
    if (template.startsWith("#gl-")) {
      return compileVugel(template, options)
    }
  }
  return compile(template, options)
}

registerRuntimeCompiler(compileToFunction)

We don't like to replace the runtime compiler as it seems like bad practice. Do you also have a suggestion for this @yyx990803?

@basvanmeurs
Copy link

Responding on my own question:

  const Demo = {
      components: {
          DemoList
      },
      data: () => {
          return {rot: 0}
      },
      render: compileVugel('#gl-demo')
  }

Instead of using the template directive we can apply the compile function and set the render function directly (which is the same as what happens when specifying a template).

@RobbinBaauw
Copy link
Author

RobbinBaauw commented Feb 20, 2020

A working version which also updates the nested components properly for the interested people:

import {Fragment, h} from 'vue'
import {effect, ref} from "@vue/reactivity";
import {defineComponent} from "@vue/runtime-core";

type VugelPropType = {
    settings: {
        w: number,
        h: number
    }
};

export const Vugel = defineComponent({
    props: {
        settings: {type: Object, default: {w: 600, h: 600}}
    },
    setup(props: VugelPropType, setupContext) {
        const elRef: Ref<HTMLElement> = ref();

        onMounted(() => {
            let rendered = false;
            let vugelRenderer: VugelRender;

            effect(() => {
                if (!rendered) {
                    rendered = true;
                    const stageOptions = {...props.settings};
                    const stage = new lng.Stage({...stageOptions, canvas: elRef.value});

                    vugelRenderer = createRendererForStage(stage);
                }

                vugelRenderer(h(Fragment, setupContext.slots.default()), vugelRenderer.stageRoot)
            });
        });

        return () => h('canvas', {class: 'custom-renderer', ref: elRef})
    }
});

where createRendererForStage returns the renderer that is created by createRenderer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants