Skip to content

Commit

Permalink
Implement background editor.
Browse files Browse the repository at this point in the history
This fixes #4.
  • Loading branch information
Haroldo de Oliveira Pinheiro committed Jul 31, 2021
2 parents e73412d + df8aeaf commit ac80104
Show file tree
Hide file tree
Showing 12 changed files with 357 additions and 20 deletions.
15 changes: 13 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"name": "vcs-game-maker",
"version": "0.5.0",
"version": "0.6.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@curtishughes/pixel-editor": "^4.0.1",
"@vue/composition-api": "^1.0.0-rc.13",
"batari-basic": "^0.0.1",
"blockly": "^6.20210701.0",
Expand Down
140 changes: 140 additions & 0 deletions src/components/PixelEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<template>
<v-card @click="handleMouse" :ripple="false">
<v-card-text>
<div class="proportion-wrapper">
<div
class="proportion-wrapper-stretcher"
:style="{'padding-bottom': 100 / aspectRatio + '%'}"
/>
<canvas
ref="editor"
class="editor-canvas"
@mousedown="handleMouse"
@mouseenter="handleMouse"
@mouseleave="handleMouse"
@mouseup="handleMouse"
@mousemove="handleMouse"
/>
</div>
</v-card-text>
<v-card-actions>
<v-btn-toggle v-model="toggledTool">
<v-btn
title="Eraser"
@click="editor.tool = eraser"
>
<v-icon>mdi-eraser</v-icon>
</v-btn>
<v-btn
title="Pencil"
@click="editor.tool = pencil"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
</v-btn-toggle>
<v-divider class="mx-4" vertical />
<v-btn
title="Undo"
@click="() => editor.undo()"
>
<v-icon>mdi-undo</v-icon>
</v-btn>
<v-btn
title="Redo"
@click="() => editor.redo()"
>
<v-icon>mdi-redo</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
import {PixelEditor, Pencil} from '@curtishughes/pixel-editor';
import {debounce} from 'lodash';
import {isMatrixEqual} from '../utils/array';
export default {
props: {
value: {type: Array, default: null},
width: {type: Number, default: 32},
height: {type: Number, default: 12},
aspectRatio: {type: Number, default: 4.0 / 3},
fgColor: {type: String, default: 'white'},
bgColor: {type: String, default: 'black'},
},
data() {
return {
pencil: new Pencil(this.fgColor),
eraser: new Pencil(this.bgColor),
toggledTool: 1,
};
},
mounted() {
const canvas = this.$refs.editor;
this.editor = new PixelEditor(canvas, this.width, this.height, this.pencil);
this.setPixels(this.value);
this.handleMouse();
// TODO: Just for testing
window.isMatrixEqual = isMatrixEqual;
},
methods: {
handleMouse: debounce(function() {
// eslint-disable-next-line no-invalid-this
const pixels = this.getPixels();
// eslint-disable-next-line no-invalid-this
if (!isMatrixEqual(this.value, pixels)) {
// eslint-disable-next-line no-invalid-this
this.$emit('input', pixels);
}
}, 300),
createEmptyPixelMatrix() {
return new Array(this.height).fill(0).map(() => new Array(this.width).fill(0));
},
getPixels() {
const pixelMatrix = this.createEmptyPixelMatrix();
this.editor.pixels.forEach((px) => {
pixelMatrix[px.y][px.x] = px.color == this.fgColor ? 1 : 0;
});
return pixelMatrix;
},
setPixels(pixelMatrix) {
pixelMatrix = pixelMatrix || this.createEmptyPixelMatrix();
const editorPixels = [];
pixelMatrix.forEach((line, y) => line.forEach((bit, x) => {
editorPixels.push({x, y, color: bit ? this.fgColor : this.bgColor});
}));
this.editor.set(editorPixels);
},
},
};
</script>
<style scoped>
.editor-canvas {
image-rendering: optimizeSpeed; /* Older versions of FF */
image-rendering: -moz-crisp-edges; /* FF 6.0+ */
image-rendering: -webkit-optimize-contrast; /* Safari */
image-rendering: -o-crisp-edges; /* OS X & Windows Opera (12.02+) */
image-rendering: pixelated; /* Awesome future-browsers */
-ms-interpolation-mode: nearest-neighbor; /* IE */
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
height: 100%;
border: 1px solid;
}
.proportion-wrapper {
position: relative;
}
.proportion-wrapper-stretcher {
width: 100%;
}
</style>
12 changes: 1 addition & 11 deletions src/generators/bbasic.bb.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,7 @@
rem - shape of the playfield and player sprites
rem **************************************************************************
playfield:
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
X....X...................X....X
X.............................X
X.............................X
X.............................X
X.............................X
X.............................X
X.............................X
X.............................X
X....X...................X....X
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
{{ playField }}
end

player0:
Expand Down
38 changes: 37 additions & 1 deletion src/generators/bbasic.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import Blockly from 'blockly/core';
import templateText from 'raw-loader!./bbasic.bb.hbs';
import Handlebars from 'handlebars';

import {useBackgroundsStorage} from '../hooks/project';
import {matrixToPlayfield} from '../utils/pixels';

const handlebarsTemplate = Handlebars.compile(templateText);

/**
Expand Down Expand Up @@ -172,11 +175,13 @@ Blockly.BBasic.finish = function(code) {
code = Object.getPrototypeOf(this).finish.call(this, code);
code = code.replace(/^[\t ]*/gm, Blockly.BBasic.INDENT);

const playField = Blockly.BBasic.generateBackgrounds();

this.isInitialized = false;

this.nameDB_.reset();
const generatedBody = definitions.join('\n\n') + '\n\n\n' + code;
return handlebarsTemplate({generatedBody});
return handlebarsTemplate({generatedBody, playField});
};

/**
Expand Down Expand Up @@ -322,6 +327,37 @@ Blockly.BBasic.getAdjusted = function(block, atId, optDelta, optNegate,
return at;
};

Blockly.BBasic.generateBackgrounds = function() {
const backgroundsStorage = useBackgroundsStorage();

let backgroundData = null;
try {
backgroundData = backgroundsStorage.value;
} catch (e) {
console.error('Failed to load backgrounds', e);
}

const backgrounds = backgroundData && backgroundData.backgrounds;
let playField =
'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n' +
'X....X...................X....X\n' +
'X.............................X\n' +
'X.............................X\n' +
'X.............................X\n' +
'X.............................X\n' +
'X.............................X\n' +
'X.............................X\n' +
'X.............................X\n' +
'X....X...................X....X\n' +
'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';

if (backgrounds && backgrounds[0] && backgrounds[0].pixels) {
playField = matrixToPlayfield(backgrounds[0].pixels);
}

return playField.split('\n').map((line) => ' ' + line).join('\n');
};

import collision from './bbasic/collision';
import colour from './bbasic/colour';
import input from './bbasic/input';
Expand Down
10 changes: 8 additions & 2 deletions src/hooks/project.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import {useLocalStorage} from '../hooks/storage';
import {useJsonLocalStorage, useLocalStorage} from '../hooks/storage';

const keyOf = (type) =>`vcs-game-maker.${type}`;

export const useProjectStorage = (type) => useLocalStorage(keyOf(type));
export const useJsonProjectStorage = (type) => useJsonLocalStorage(keyOf(type));

export const useProjectStorage = (type) => useLocalStorage(`vcs-game-maker.${type}`);
export const useWorkspaceStorage = () => useProjectStorage('workspace');
export const useBackgroundsStorage = () => useJsonProjectStorage('backgrounds');

10 changes: 10 additions & 0 deletions src/hooks/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,13 @@ export const useLocalStorage = (key) => computed({
localStorage.setItem(key, value);
},
});

export const useJsonLocalStorage = (key) => computed({
get() {
const jsonText = localStorage.getItem(key);
return jsonText ? JSON.parse(jsonText) : null;
},
set(value) {
localStorage.setItem(key, JSON.stringify(value));
},
});
5 changes: 5 additions & 0 deletions src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ const routes = [
component: () => import(/* webpackChunkName: "about" */
'../views/About.vue'),
},
{
path: '/background',
name: 'Background',
component: () => import('../views/BackgroundEditor.vue'),
},
{
path: '/generated',
name: 'Generated',
Expand Down
25 changes: 25 additions & 0 deletions src/utils/array.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Based on https://stackoverflow.com/a/16436975/679240
export const isArrayEqual = (a, b) => {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;

for (let i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
};

export const isMatrixEqual = (a, b) => {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;

for (let i = 0; i < a.length; ++i) {
if (!isArrayEqual(a[i], b[i])) return false;
}

return true;
};

export const copyMatrix = (original) => original.map((row) => row.slice());
6 changes: 6 additions & 0 deletions src/utils/pixels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const playfieldToMatrix = (text) => text.trim().split('\n')
.map((line) => line.trim().split('').map((ch) => ch === 'X' ? 1 : 0));

export const matrixToPlayfield = (matrix) => matrix
.map((line) => line.map((pixel) => pixel ? 'X' : '.').join(''))
.join('\n').trim();
Loading

0 comments on commit ac80104

Please sign in to comment.