Skip to content
  1. Examples

PixiJs integration

This example shows integration with PixiJs rendering library. It allows, once added as an Ogma layer to efficiently draw custom shapes and images.

ts
import Ogma, { Layer } from '@linkurious/ogma';
import * as PIXI from 'pixi.js';
import { GUI } from 'dat.gui';
import { runLayout } from '../iphone-parts/layout';

const ogma = new Ogma({
  container: 'graph-container'
});

const NODE_COLOR = '#1E1E32';
const NODE_COLOR_TRANSPARENT = 'rgba(30, 30, 50, 0.5)';

ogma.styles.addNodeRule({
  color: NODE_COLOR
});

/**
 * @typedef {Object} OgmaPixiLayer
 * @extends {Layer}
 * @property {Layer} layer Ogma `layer` controller. Use it to change the z-index.
 * @property {Pixi.App} app The stage of this app is in Ogma coordinate system
 * @property {() => void} destroy Destroy wrapper: removes the Pixi.js layer
 * from Ogma and stops the view synchronisation
 */
interface OgmaPixiLayer extends Layer {
  app: PIXI.Application;
  animations: Record<string, PIXI.AnimatedSprite>;
  destroy: () => this;
}

/**
 * Adds a Pixi.js layer to Ogma that is synchronised with the
 * camera position and movement
 */
async function addPixijsLayer(ogma: Ogma) {
  const { width, height } = ogma.view.get();
  const app = new PIXI.Application();
  await app.init({
    backgroundAlpha: 0,
    transparent: true,
    background: '0x00000000',
    width,
    height
  });
  const ogmaLayer = ogma.layers.addLayer(app.canvas);
  const synchronizeViews = () => {
    const { x, y, zoom, width, height } = ogma.view.get();
    app.stage.x = -x * zoom;
    app.stage.y = -y * zoom;
    app.stage.scale = { x: zoom, y: zoom };
    app.stage.pivot = { x: -width / 2 / zoom, y: -height / 2 / zoom };
    app.renderer.render(app.stage);
  };
  ogma.events
    .on('frame', synchronizeViews)
    .on('dragProgress', () => update(layer, ogma));

  const size = { width: 160, height: 160 };
  const spriteData = {
    frames: {},
    meta: {
      image: 'files/sprites.png',
      size: {
        w: 640,
        h: 1280
      }
    },
    animations: {}
  };
  const animations = ['right', 'top', 'left', 'bottom', 'sitting', 'hunting'];
  animations.forEach((animation, i) => {
    const y = i * size.height;
    const arr: string[] = [];
    for (let j = 0; j < 4; j++) {
      const x = j * size.width;
      const frame = {
        frame: { x, y, w: size.width, h: size.height },
        spriteSourceSize: { x: 0, y: 0, w: size.width, h: size.height },
        sourceSize: { w: size.width, h: size.height }
      };
      const frameName = `${animation}${j}`;
      // @ts-expect-error
      spriteData.frames[frameName] = frame;
      arr.push(frameName);
    }
    // @ts-expect-error
    spriteData.animations[animation] = arr;
  });

  const texture = await PIXI.Assets.load(spriteData.meta.image);

  const spritesheet = new PIXI.Spritesheet(texture, spriteData);

  await spritesheet.parse();

  return {
    ...ogmaLayer,
    animations: spritesheet.animations,
    app,
    destroy() {
      ogma.events.off(synchronizeViews);
      ogmaLayer.destroy();
      return this;
    }
  } as OgmaPixiLayer;
}

const layer = await addPixijsLayer(ogma);

/**
 * Renders additional markup with Pixi.js
 */
function render(layer: OgmaPixiLayer, ogma: Ogma) {
  const container = new PIXI.Container();
  layer.app.stage.addChild(container);
  const animations = layer.animations;
  const names = Object.keys(animations);
  ogma.getNodes().forEach((node, i) => {
    const animation = animations[names[i % names.length]];
    const pixiSprite = new PIXI.AnimatedSprite(animation);
    pixiSprite.animationSpeed = 0.1;
    pixiSprite.play();
    pixiSprite.anchor.set(0.5, 0.75);
    pixiSprite.x = node.getPosition().x;
    pixiSprite.y = node.getPosition().y;
    pixiSprite.scale.set(0.09);
    container.addChild(pixiSprite);
  });
}

/**
 * Updates the shapes created with PixiJS
 * @param {OgmaPixiLayer} layer
 * @param {Ogma} ogma
 */
function update(layer: OgmaPixiLayer, ogma: Ogma) {
  const container = layer.app.stage.children[0];
  const sprites = container.children;
  ogma
    .getNodes()
    .getAttributes(['x', 'y'])
    .forEach(({ x, y }, i) => {
      sprites[i].x = x;
      sprites[i].y = y;
    });
}

const settings = {
  over: true,
  hideLayer: () => layer.hide(),
  showLayer: () => layer.show(),
  removeLayer: () => layer.destroy(),
  runLayout: async () => {
    layer.hide();
    await ogma.layouts.force({ locate: true });
    update(layer, ogma);
    layer.show();
  }
};
const gui = new GUI();
gui.add(settings, 'hideLayer').name('Hide layer');
gui.add(settings, 'showLayer').name('Show layer');
gui.add(settings, 'runLayout').name('Run layout');
gui.add(settings, 'removeLayer').name('Remove layer');
gui
  .add(settings, 'over')
  .name('Over/under')
  .onChange(value => {
    if (value) {
      layer.moveToTop();
      ogma.getNodes().setAttribute('color', undefined);
    } else {
      layer.moveToBottom();
      ogma.getNodes().setAttribute('color', NODE_COLOR_TRANSPARENT);
    }
  });

const graph = await ogma.generate.grid({ rows: 4, columns: 4 });
await ogma.setGraph(graph);
await ogma.view.locateGraph();
render(layer, ogma);
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <link type="text/css" rel="stylesheet" href="styles.css" />
    <script src="https://cdn.jsdelivr.net/npm/pixi.js@8.3.4/dist/pixi.min.js"></script>
    <script type="importmap">
      {
        "imports": {
          "pixi.js": "https://cdn.jsdelivr.net/npm/pixi.js@8.3.4/dist/pixi.min.mjs"
        }
      }
    </script>
  </head>

  <body>
    <div id="graph-container"></div>
    <script type="module" src="index.ts"></script>
  </body>
</html>
css
#graph-container {
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: 0;
  overflow: hidden;
}