~kylep/visual-cell-lang

7a29e071c12ca7ab3b1e94fc72740235a9f6e61f — Kyle Perik 1 year, 8 months ago b00a81e
Add editor
4 files changed, 165 insertions(+), 24 deletions(-)

M dist/main.css
M src/game.js
M src/lang.js
M src/main.js
M dist/main.css => dist/main.css +6 -1
@@ 12,7 12,12 @@ body {
}

body {
    flex-direction: column;
    flex-direction: row;
    width: 100%;
}

.toolbar {
    flex-grow: 1;
}

canvas {

M src/game.js => src/game.js +43 -6
@@ 4,7 4,7 @@ import {TILE_SIZE, FONT_SIZE, TICK_RATE} from './main'

const DATA_SPEED = .1

export function drawConnection(connection) {
function getConnectionPath (connection) {
  const { startBox, startOutput, endBox, endInput, g } = connection;
  const outputIndex = startBox.outputs.indexOf(startOutput);
  const start = (


@@ 22,12 22,23 @@ export function drawConnection(connection) {
        0,
      ))
  );

  return {
    start, end
  };
}

export function drawConnection(connection) {
  const { start, end } = getConnectionPath(connection);
  const g = connection.g;
  g.clear();
  g.lineStyle(TILE_SIZE, hsv(.1, .2, .4));
  g.moveTo(start.x * TILE_SIZE, start.y * TILE_SIZE);
  g.lineTo(end.x * TILE_SIZE, end.y * TILE_SIZE);
  connection.start = start;
  connection.end = end;

  g.hitArea = g.getBounds();
}

export function removeConnection(connection, gameData, gs) {


@@ 41,6 52,15 @@ export function addConnection(data, gameData, gs) {
    ...data,
    g: new PIXI.Graphics(),
  };
  connection.g.interactive = true;
  connection.g.on('rightdown', (e) => {
    const { start, end } = getConnectionPath(connection);
    const path = end.sub(start);
    const distance = gameData.mouse.mul(1 / TILE_SIZE).sub(end).project(path.perp());
    if (Math.abs(distance) < 0.5) {
      removeConnection(connection, gameData, gs);
    }
  });
  gameData.connections.push(connection);
  drawConnection(connection);
  gs.connections.addChild(connection.g);


@@ 134,7 154,8 @@ export function drawBox(box) {

export function addBox(data, gameData, gs) {
  const pos = new Vec(Math.floor(data.pos.x), Math.floor(data.pos.y))
  const boxDef = gameData.boxDefinitions[data.type];
  const customDef = gameData.customDefinitions[data.type];
  const boxDef = gameData.boxDefinitions[data.type] || customDef;
  if (!boxDef) {
    console.error(`No box definition for "${data.type}"`);
  }


@@ 148,17 169,28 @@ export function addBox(data, gameData, gs) {
    clicked: false,
    label: '',
  };
  const boxData = typeof data.literalValue !== 'undefined' ? { value: data.literalValue } : boxDef.data;
  const boxData = (
    typeof data.literalValue !== 'undefined'
      ? { value: data.literalValue }
    : (boxDef.data || {})
  );
  const box = {
    ...defaults,
    ...data,
    ...boxDef,
    js: !customDef,
    data: boxData,
    pos,
  }
  gameData.codeBoxes.push(box);
  drawBox(box);
  gs.boxes.addChild(box.g);

  // Run init script
  if (box.init) {
    interpret(box.init, { value: null, data: box.data })
  }

  return box
}



@@ 225,7 257,7 @@ export function evaluateNewPieces (newKey, gameData, gs) {
      return []
    }
    box.triggered = false;
    const boxDef = gameData.boxDefinitions[box.type];
    const boxDef = gameData.boxDefinitions[box.type] || gameData.customDefinitions[box.type];
    if (boxDef.iterateData) {
      box.data = boxDef.iterateData({box, data: box.data, ...inputValues});
    }


@@ 235,7 267,7 @@ export function evaluateNewPieces (newKey, gameData, gs) {
      // output event like: { value: value, output: outputType }
      // and queue it for output
      // Then returns the value again
      const outputEvents = [];
      let outputEvents = [];
      const out = (boxDef.outputs || []).reduce((r, outputType) => {
        r[outputType] = (value) => {
          const event = {


@@ 252,7 284,12 @@ export function evaluateNewPieces (newKey, gameData, gs) {
          return;
        }
        const args = { value, out, data: box.data, box };
        func(args);
        if (box.js) {
          func(args);
        } else {
          const result = interpret(func, { value, data: box.data });
          outputEvents = result.output;
        }
      });
      box.inputQueue = [];
      return outputEvents.flatMap(({ value, output }) =>  emit(value, box, output));

M src/lang.js => src/lang.js +4 -1
@@ 28,11 28,14 @@ export function parse (code) {
        }
      }
    } else {
      const newContents = line.slice(2);
      return {
        ...r,
        queuedBranch: {
          ...r.queuedBranch,
          contents: r.queuedBranch.contents + line.slice(2)
          contents: r.queuedBranch.contents
            ? [r.queuedBranch.contents, newContents].join(' ')
            : newContents
        }
      }
    }

M src/main.js => src/main.js +112 -16
@@ 1,6 1,6 @@
import * as PIXI from 'pixi.js'
import {Vec, range, nrand, choose, hsv, shuffle, approach} from './utils'
import {addConnection, removeConnection, drawConnection, addBox, removeBox, drawBox, evaluateNewPieces} from './game'
import {addConnection, removeConnection, drawConnection, addBox, resetBox, removeBox, drawBox, evaluateNewPieces} from './game'
import {loadCodeBox, boxDefinitions} from './codebox'
import {interpret, parse} from './lang'



@@ 14,7 14,30 @@ PIXI.Loader.shared.add('Pixelzim', 'pixelzim_3x5_7.fnt')
PIXI.Loader.shared.add('CompassPro', 'compasspro_medium_12.fnt')
PIXI.Loader.shared.add('MatchupPro', 'matchuppro_medium_12.fnt')

let innerSize = Math.min(window.innerWidth, window.innerHeight - 30)
const $ = (selector) => document.querySelector(selector);
const el = ({ kind, children, text, value, attributes, classes }) => {
  const element = document.createElement(kind || 'div');
  if (text) {
    element.textContent = text;
  }
  if (value) {
    element.value = value;
  }
  (children || []).forEach(child => {
    element.appendChild(child);
  });
  (Object.keys(attributes || {})).forEach(key => {
    element.setAttribute(key, attributes[key]);
  });
  element.classNames = classes || '';
  return element;
}

const toolbarWidth = 300;
let innerSize = Math.min(
  window.innerWidth - toolbarWidth,
  window.innerHeight,
)
let psize = 1
let height = innerSize / psize
let width = innerSize / psize


@@ 47,7 70,7 @@ window.world = world

app.renderer.backgroundColor = hsv(.6, .1, .9)

document.querySelector('.canvas').appendChild(app.view)
$('.canvas').appendChild(app.view)

//Start the game loop
setInterval(() => gameLoop(1), 30)


@@ 71,6 94,7 @@ export let gameData = {
  connections: [],
  dataPieces: [],
  boxDefinitions: [],
  customDefinitions: {},
  moving: null,
  drawingConnection: null,
  gameSpeed: 2,


@@ 127,10 151,10 @@ function save () {
      newConnection.startBoxIndex = gameData.codeBoxes.indexOf(connection.startBox);
      newConnection.endBoxIndex = gameData.codeBoxes.indexOf(connection.endBox);
      return newConnection;
    })
    }),
    customDefinitions: gameData.customDefinitions,
  });
}
window.save = save;

function load () {
  const rawSave = localStorage.save;


@@ 138,6 162,7 @@ function load () {
    return;
  }
  const save = JSON.parse(rawSave);
  gameData.customDefinitions = save.customDefinitions;
  save.boxes.forEach(box => addBox(box, gameData, gs));
  save.connections.map(connection => ({
    ...connection,


@@ 146,18 171,34 @@ function load () {
  })).forEach(connection => addConnection(connection, gameData, gs));
}

function reset () {
  save();
  Array.from(gameData.codeBoxes).forEach(box => removeBox(box, gameData, gs));
  gameData.dataPieces.forEach(piece => gs.dataPieces.removeChild(piece.g));
  gameData.dataPieces = [];
  Array.from(gameData.connections).forEach(connection => removeConnection(piece, gameData, gs))
  load();
}

window.save = save;
window.load = load;

function initUI() {
  const toolbarEl = document.querySelector('.toolbar');
  const codeTypeDropdown = document.createElement('select');
  Object.keys(boxDefinitions).forEach(def => {
    const optionEl = document.createElement('option');
    optionEl.textContent = def;
    optionEl.value = def;
    codeTypeDropdown.appendChild(optionEl);
  })
  const toolbarEl = $('.toolbar');

  const codeTypeDropdown = el({ kind: 'select' });
  function buildTypeDropdown () {
    codeTypeDropdown.innerHTML = '';
    const definitions = Object.keys(gameData.boxDefinitions).concat(Object.keys(gameData.customDefinitions));
    definitions.map(
      def => el({ kind: 'option', text: def, value: def })
    ).forEach(option => {
      codeTypeDropdown.appendChild(option);
    })
  }
  buildTypeDropdown();
  toolbarEl.appendChild(codeTypeDropdown);
  const codeBoxButton = document.createElement('button');
  codeBoxButton.textContent = 'Add';
  const codeBoxButton = el({ kind: 'button', text: 'Add' });
  codeBoxButton.addEventListener('click', () => {
    const pos = new Vec(width / TILE_SIZE / 2, height / TILE_SIZE - 15);
    if (['literal', 'literalFloat'].includes(codeTypeDropdown.value)) {


@@ 171,6 212,61 @@ function initUI() {
    }
  });
  toolbarEl.appendChild(codeBoxButton);

  const editorTextbox = el({ kind: 'textarea' });
  const editorSaveButton = el({ kind: 'button', text: 'Save' });
  const codeEditorEl = el({
    kind: 'div',
    children: [
      el({ kind: 'label', text: 'editor ' }),
      editorSaveButton,
      el({
        kind: 'div',
        children: [
          editorTextbox
        ]
      })
    ]
  });
  toolbarEl.appendChild(codeEditorEl);
  editorSaveButton.addEventListener('click', () => {
    const code = editorTextbox.value.split('\n');
    const name = code[0];
    const [inputSig, outputSig] = code[1].split(' -> ');

    // Extra line at the end just in case
    const rest = code.slice(2).join('\n') + '\n';
    const parsedCode = parse(rest);
    const inputs = inputSig.split(' ').reduce((r, input) => {
      const inputBranch = parsedCode.branches[input];
      if (!inputBranch) {
        console.error(`No input handler defined for custom.${input}`)
      }
      r[input] = inputBranch || '';
      return r;
    }, {});
    const outputs = outputSig.split(' ').filter(x => x);
    const init = parsedCode;
    gameData.customDefinitions[name] = {
      init,
      inputs,
      outputs,
      code: editorTextbox.value
    };
    buildTypeDropdown();
  });

  codeTypeDropdown.addEventListener('change', () => {
    const customDef = gameData.customDefinitions[codeTypeDropdown.value];
    if (customDef) {
      editorTextbox.value = customDef.code;
    }
  });
  const resetButtonEl = el({ kind: 'button', text: 'Reset' });
  resetButtonEl.addEventListener('click', () => {
    reset();
  });
  toolbarEl.appendChild(resetButtonEl);
}

function setup () {


@@ 299,7 395,6 @@ def pop:
    }
  });

  initUI();
  app.view.tabIndex = 0
  app.view.focus()
  app.view.addEventListener('contextmenu', contextmenu)


@@ 345,6 440,7 @@ def pop:
  gameData.boxDefinitions = boxDefinitions;

  load();
  initUI();

  setInterval(save, 10000);