Hone logo
Hone
Problems

Vue 3 WebGL Renderer Integration

This challenge asks you to integrate a WebGL rendering context within a Vue 3 application. You will create a reusable Vue component that manages a WebGL canvas, allowing for the rendering of basic 2D or 3D graphics. This is a fundamental task for building interactive visualizations, games, or complex UIs with WebGL.

Problem Description

Your goal is to build a Vue 3 component, VueWebglRenderer, that takes a WebGL rendering function as a prop. This component should:

  1. Initialize and Manage WebGL Context: Create a <canvas> element and obtain its WebGL rendering context (preferably webgl2 if available, otherwise webgl).
  2. Handle Resizing: Ensure the canvas correctly resizes with its parent container, updating the WebGL viewport accordingly.
  3. Execute Rendering Logic: Accept a function prop (e.g., renderFn) which will be responsible for all WebGL drawing operations. This function should be called whenever the canvas needs to be rendered or updated.
  4. Lifecycle Management: Properly clean up WebGL resources (e.g., shaders, buffers, textures) when the component is unmounted to prevent memory leaks.
  5. Event Handling (Optional but Recommended): Provide a mechanism to pass DOM events from the canvas to the parent component or to the renderFn if needed.

Key Requirements:

  • The component must be written in TypeScript.
  • It should be a functional Vue 3 component using the Composition API.
  • The renderFn prop should receive the WebGLRenderingContext and the dimensions of the canvas as arguments.
  • The component should expose a canvasRef to allow parent components to access the underlying canvas element if necessary.

Expected Behavior:

When the VueWebglRenderer component is used, it should display a canvas. The provided renderFn will be executed, drawing whatever is defined within it onto the canvas. The canvas should respond to parent container resizing.

Edge Cases to Consider:

  • WebGL context creation failure.
  • Browser compatibility (older browsers might not support webgl2).
  • Ensuring cleanup of all WebGL resources.

Examples

Example 1: Rendering a Simple Triangle

<template>
  <div style="width: 400px; height: 300px; border: 1px solid black;">
    <VueWebglRenderer :renderFn="renderTriangle" />
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import VueWebglRenderer from './components/VueWebglRenderer.vue'; // Assuming component is here

export default defineComponent({
  components: {
    VueWebglRenderer,
  },
  setup() {
    const renderTriangle = (gl: WebGLRenderingContext, width: number, height: number) => {
      // Basic setup
      gl.clearColor(0.0, 0.0, 0.0, 1.0);
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.viewport(0, 0, width, height);

      // ... (code to compile shaders, create buffers, draw triangle)
      // For simplicity, this example omits shader and buffer creation.
      // In a real scenario, you'd have:
      // const program = createShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
      // gl.useProgram(program);
      // const positionBuffer = gl.createBuffer();
      // gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
      // const positions = [-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0];
      // gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
      // const positionAttribLocation = gl.getAttribLocation(program, 'a_position');
      // gl.enableVertexAttribArray(positionAttribLocation);
      // gl.vertexAttribPointer(positionAttribLocation, 3, gl.FLOAT, false, 0, 0);
      // gl.drawArrays(gl.TRIANGLES, 0, 3);

      console.log("Rendering triangle on canvas of size:", width, height);
    };

    return {
      renderTriangle,
    };
  },
});
</script>

Expected Output: A black canvas area with a red triangle drawn (assuming standard WebGL triangle rendering code, which would be part of a more complete renderTriangle implementation). The console would log "Rendering triangle on canvas of size: 400 300". If the parent div is resized to 600x400, the log would reflect those dimensions, and the triangle would be redrawn accordingly.

Example 2: Handling Context Loss

// Inside VueWebglRenderer.vue component
import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue';
import type { PropType } from 'vue';

export type WebGLRenderFunction = (gl: WebGLRenderingContext, width: number, height: number) => void;

export default defineComponent({
  name: 'VueWebglRenderer',
  props: {
    renderFn: {
      type: Function as PropType<WebGLRenderFunction>,
      required: true,
    },
  },
  setup(props) {
    const canvasRef = ref<HTMLCanvasElement | null>(null);
    let gl: WebGLRenderingContext | null = null;
    let animationFrameId: number | null = null;

    const resizeObserver = new ResizeObserver(entries => {
      for (const entry of entries) {
        const { width, height } = entry.contentRect;
        if (gl) {
          gl.canvas.width = width;
          gl.canvas.height = height;
          props.renderFn(gl, width, height);
        }
      }
    });

    const handleContextLoss = () => {
      console.warn('WebGL context lost.');
      // In a real app, you might want to reinitialize everything here
      // or inform the parent component to handle it.
      if (animationFrameId !== null) {
        cancelAnimationFrame(animationFrameId);
      }
    };

    const handleContextRestored = () => {
      console.log('WebGL context restored.');
      // Reinitialize WebGL resources and resume rendering
      initializeWebGL();
      animationFrameId = requestAnimationFrame(renderLoop);
    };

    const initializeWebGL = () => {
      if (!canvasRef.value) return;

      const contextNames = ['webgl2', 'webgl', 'experimental-webgl'];
      let context = null;
      for (const name of contextNames) {
        context = canvasRef.value.getContext(name) as WebGLRenderingContext;
        if (context) break;
      }

      if (!context) {
        console.error('Unable to initialize WebGL. Your browser may not support it.');
        return;
      }
      gl = context;

      // Enable context loss/restore events
      canvasRef.value.addEventListener('webglcontextlost', handleContextLoss, false);
      canvasRef.value.addEventListener('webglcontextrestored', handleContextRestored, false);

      // Initial render on mount
      const { width, height } = canvasRef.value.getBoundingClientRect();
      gl.canvas.width = width;
      gl.canvas.height = height;
      props.renderFn(gl, width, height);
    };

    const renderLoop = () => {
      if (!gl) return;
      const { width, height } = gl.canvas;
      props.renderFn(gl, width, height);
      animationFrameId = requestAnimationFrame(renderLoop);
    };

    onMounted(() => {
      if (canvasRef.value) {
        resizeObserver.observe(canvasRef.value.parentElement!);
        initializeWebGL();
        // Optionally start an animation loop if renderFn is intended for continuous updates
        // animationFrameId = requestAnimationFrame(renderLoop);
      }
    });

    onUnmounted(() => {
      if (canvasRef.value) {
        resizeObserver.unobserve(canvasRef.value.parentElement!);
      }
      if (gl) {
        // Cleanup WebGL resources (shaders, buffers, etc.)
        // This is a crucial part and depends on what `renderFn` creates.
        // For a robust component, you might want to expose cleanup methods
        // or have `renderFn` return a cleanup function.
        console.log('Cleaning up WebGL resources.');
      }
      if (animationFrameId !== null) {
        cancelAnimationFrame(animationFrameId);
      }
      if (canvasRef.value) {
        canvasRef.value.removeEventListener('webglcontextlost', handleContextLoss);
        canvasRef.value.removeEventListener('webglcontextrestored', handleContextRestored);
      }
    });

    // Re-render if renderFn prop changes
    watch(() => props.renderFn, () => {
      if (gl) {
        const { width, height } = canvasRef.value!;
        props.renderFn(gl, width, height);
      }
    });

    return {
      canvasRef,
    };
  },
});

Expected Behavior: The component gracefully handles a WebGL context loss event, logs a warning, and attempts to restore it. Upon restoration, it reinitializes and resumes rendering.

Constraints

  • The solution must be implemented in TypeScript.
  • The component should be a Vue 3 functional component using the Composition API.
  • The renderFn prop is mandatory and must be a function.
  • The component should ideally try to obtain webgl2 context before falling back to webgl.
  • Performance: The component should avoid unnecessary re-renders and ensure efficient WebGL resource management. Rendering operations should be tied to actual drawing needs, not just component updates.
  • The canvasRef should be accessible for direct manipulation if needed by the parent.

Notes

  • This challenge focuses on the integration of WebGL into Vue, not on writing complex WebGL shaders or rendering pipelines. You can assume a renderFn will be provided that knows how to draw something.
  • For cleanup, consider that the renderFn might create WebGL objects (buffers, shaders, textures, programs). A more advanced component might require renderFn to return a cleanup function or have explicit methods to manage resource lifecycles. For this challenge, a basic console.log for cleanup is acceptable, but understanding the need for proper resource deallocation is key.
  • The ResizeObserver is a modern and efficient way to handle element resizing.
  • Consider the potential for parentElement to be null if the component is rendered in an unusual context.
Loading editor...
typescript