JSFiddle - React, Tailwind, and code Playground

by Sam Wray

HTML

<div id="app">
  <p>Contrived OffscreenCanvas example</p>
  Offscreen:
  <canvas ref="canvas"></canvas>
  <br>
  (Onscreen?):
  <canvas ref="oncanvas"></canvas>
  <br><br>
  <button @click="displayFib">Fibonacci 👌</button><br>
  Fibonacci * {{ this.fibNum }} = {{ this.fib }}
  <br>
  <button @click="videoPlay">{{ paused ? 'Play' : 'Pause' }} video</button>
  <hr>
  <h2>Registered Modules</h2>

  <ul>
    <li
      v-for="(module, moduleName) in registeredModules"
    >
      {{ moduleName }}
      <input
        type="button"
        @click="toggleActiveModule($event, moduleName)"
        :value="$store.state.active.indexOf(moduleName) > -1 ? 'deactivate' : 'activate'"
      >
    </li>
  </ul>

  <hr>
  <h2>Module Controls</h2>

  <div v-for="module in activeModules">
    <h3>{{ module }}</h3>
    <label
      v-for="(prop, propName) in $store.state.registry[module].props"
    >
      {{ propName }}
      <input
        :type="jsTypeToInputType(prop.type)"
        :min="prop.min"
        :max="prop.max"
        :value="$store.state.registry[module][propName]"
        @input="updateModuleProp($event, propName, module)"
      >
    </label>
  </div>

  <hr>
  <button @click="toggleStates">{{ showStates ? 'Hide' : 'Show' }} states</button>
  <div class="flex" v-if="showStates">
    <div>
      <h2>Vuex state</h2>
      <pre>{{ $store.state }}</pre>
    </div>

    <div>
      <h2>Worker state</h2>
      <pre>{{ JSON.stringify(workerState, null, 2) }}</pre>
    </div>
  </div>
</div>
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vuex"></script>
<script src="https://unpkg.com/[email protected]/umd/comlink.js"></script>

CSS

body {
  font-family: Input, Menlo, monospace;
  background-color: #ddd;
  font-size: 20px;
}

canvas {
  background-color: #000;
}

.flex {
  display: flex;
}

.flex > * {
  flex-grow: 1;
}

JavaScript

const module1 = {
  meta: {
    name: 'Module 1',
  },
  props: {
    textSize: {
      type: 'float',
      min: 0,
      max: 100,
      default: 30,
    },
    text: {
      type: 'string',
      default: 'đź‘˝',
    },
  },
  draw({ canvas, context, delta }) {
    const { width, height } = canvas;

    context.font = `${this.textSize}pt sans-serif`;
    context.textAlign = 'center';
    context.fillStyle = 'rgba(0,0,0,0.11)';
    context.fillRect(0, 0, width, height);
    context.fillStyle = `hsl(${delta / 40}, 80%, 50%)`;
    context.fillText(
      this.text,
      (width / 2) + ((10 * Math.sin(delta / 400)) * (7 * -Math.sin(delta / 380))),
      (height / 2) + ((18 * -Math.cos(delta / 100)) * (2 * Math.cos(delta / 480)))
    );
  },
};

const module2 = {
  meta: {
    name: 'Module 2',
  },
  props: {
    circleSize: {
      type: 'float',
      min: 0,
      max: 100,
      default: 50,
    },
  },
  draw: function({ canvas, context, delta }) {
    const { width, height } = canvas;

    context.fillStyle = `hsla(${delta / 100}, 50%, 50%, 0.15)`;
    context.fillRect(
      0,
      0,
      width,
      height
    );

    context.fillStyle = `hsla(${delta / 200}, 50%, 80%, 1)`;
    context.beginPath();
    context.arc(
      ((width / 2) * (-Math.sin(delta / 1000) + 1)),
      ((height / 2) * (-Math.cos(delta / 1000) + 1)),
      this.circleSize,
      0,
      Math.PI * 2
    );
    context.fill();
  },
};

//////

const workerScript = `
  const state = {
    registry: {},
    active: [],
  };

  let canvas;
  let context;
  let frame;

  function render(delta) {
    requestAnimationFrame(render);
    if (!canvas) return;

    const { width, height } = canvas;
		
    if (frame) context.drawImage(frame, 0, 0, width, height);
    
    state.active.forEach((moduleName) => {
      state.registry[moduleName].draw({
        canvas,
        context,
        delta: Date.now(),
      });
    });
  }
  requestAnimationFrame(render);

  self.onmessage = function(e) {
...