Readme update, removed font awesome (we now have material icons), front end css review

This commit is contained in:
Hugh Bord 2021-12-21 19:03:40 +10:00
parent 28479ccd80
commit 90010a8831
16 changed files with 396 additions and 545 deletions

View File

@ -28,29 +28,34 @@ A most latest production build to use is available at https://asciibird.jewbird.
* Show and hide layers
* Change layer order
* Double click to rename layer
* Copy and paste ASCII blocks between tabs
* Context menu for layers
* Copy and paste ASCII blocks between tabs with the select tool
* Remembers state on refresh and when the browser loads, can also export the state to a file and load elsewhere.
* So you never lose your ascii art!
* Saves layers, brushes data also to same file
* Can import from clipboard, load from irc.watch/ascii, load from file
* Can export mirc ascii to clipboard, file or HTTP POST
* 99 Colour support, swap colours with button click
* 99 Colour support
* Swap fg and bg colours with button click or alt + k
* Mirror X and Y
* Grid mode with alt + g
* Undo and redo with ctrl + z and ctrl + y, undos are set to a limit of 50 at the moment.
* Undo and redo with ctrl + z and ctrl + y, undos are set to a limit of 200 at the moment.
* Fg, Bg and Char boxes to filter when using certain tools
* For example filling with Char unchecked will ignore characters when filling
* Image overlay to trace images
* If you want to remove the background but keep the text, unchek FG and Char and eraser the bg only.
* Image overlay to trace images
* Accepts URLs only at the moment
* Can adjust the size and properties
* Toolbar containing
* Select
* Text mode
* Select, to copy and paste blocks as brushes
* Text mode, with arrow key support
* Fill background blocks
* Brush mode, can be controlled with keyboard and mouse
* Block picker (grab fg, bg and char of a block)
* Eraser - remove blocks, can be controlled with keyboard and mouse
* Fill Eraser - Fill remove blocks by bg, fg or char
* Brush Library and History
* Make circle, square and cross brushes by sizes
* Make circle, square, cross and other brushes by sizes
* Brush history, can save or re-use old brushes
* Library - Save most used brushes to library
* Brush history is set to a limit of 50
@ -59,24 +64,27 @@ A most latest production build to use is available at https://asciibird.jewbird.
* Clicking updates block
* Right clicking removes block
* Hovering outside brush area will save brush to history
* Context menu available on all brushes preview areas
* Export any brush to PNG, mIRC clipboard or file by right clicking the brush preview
# Roadmap and Bug To Fixes
## Noted Bugs to Fix
ASCIIBIRD is mostly usable. There are some bugs however to note at the moment. Refreshing the page seems to fix most strange things.
* Layers manipulation and undo / redo is buggy
## Focusing on Now
* Importer could be re-written with regex
* More Context Menus (right click menu)
* Brushes Canvas right click
* ASCII right click
## Roadmap
## Features to Add
* Layers undo and redo could be implemented, at the moment there isn't any.
* Warning on mirc export if ascii exceeds IRCs 512 per chat line limit.
* Review encodings check on file import - UTF8 vs Latin something
* More Context Menus (right click menu)
* ASCII right click
## Bugs to fix
* If you apply an empty block from a brush, it will remove the char when it is supposed to leave the block alone.
* A bigger circle brush is a good example for this one.
* Can't type in dialogs
* Still cannot change width in edit ascii modal
## Mobile / Touch Screen support
Doesn't exist at the moment. While the underlying functions and code is compatible with mobile browsers from *babel*, the touch canvas events and text will need to be reviewed to work better with touch screens. For example while you can brush once, you cannot move the brush around.
# Keyboard Shortcuts
@ -126,7 +134,7 @@ The toolbar keyboard shorts are used with the ALT key.
### Eraser Mode Only
* Four arrow keys control text cursor
* Space - apply brush
* Space - apply eraser
### Brush Mode Only

View File

@ -8,10 +8,6 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-regular-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/vue-fontawesome": "^2.0.2",
"@tailwindcss/forms": "^0.3.3",
"@tailwindcss/postcss7-compat": "^2.2.7",
"autoprefixer": "^9",

View File

@ -10,7 +10,6 @@
<KeyboardShortcuts
:selected-blocks="selectedBlocks"
:text-editing="textEditing"
:selecting="selecting"
:is-showing-dialog="isShowingDialog"
@updatecanvas="updatecanvas"
@ -33,7 +32,7 @@
<t-input v-model="lastPostURL" />
</t-dialog>
<context-menu :display="showContextMenu" ref="menu" class="z-50">
<context-menu ref="menu" class="z-50">
<ul>
<li
@click="$store.commit('openModal', 'new-ascii')"
@ -147,6 +146,7 @@
/>
<Toolbar :y-offset="scrollOffset" />
<DebugPanel
:canvas-x="canvasX"
:canvas-y="canvasY"
@ -154,7 +154,7 @@
:y-offset="scrollOffset"
/>
<BrushLibrary v-if="brushLibraryState.visible" :y-offset="scrollOffset" />
<BrushLibrary v-show="brushLibraryState.visible" :y-offset="scrollOffset" />
<BrushPreview @inputtingbrush="inputtingbrush" :y-offset="scrollOffset" />
@ -184,9 +184,6 @@
@mouseup.right="openContextMenu"
@contextmenu.prevent
>
<!-- <h1 class="text-4xl">ASCIIBIRD</h1>
<h3>Right click to start</h3> -->
<BrushCanvas :blocks="splashAscii" />
</div>
</template>

View File

@ -151,43 +151,35 @@ export const charCodes = [' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*'
// Toolbar icons
export const toolbarIcons = [{
name: 'default',
icon: 'mouse-pointer',
fa: 'fas',
icon: 'edit_off',
},
{
name: 'select',
icon: 'square',
fa: 'far',
icon: 'photo_size_select_small',
},
{
name: 'text',
icon: 'font',
fa: 'fas',
icon: 'text_rotation_none',
},
{
name: 'fill',
icon: 'fill-drip',
fa: 'fas',
icon: 'format_color_fill',
},
{
name: 'brush',
icon: 'paint-brush',
fa: 'fas',
icon: 'brush',
},
{
name: 'dropper',
icon: 'eye-dropper',
fa: 'fas',
icon: 'colorize',
},
{
name: 'eraser',
icon: 'eraser',
fa: 'fas',
icon: 'remove_circle_outline',
},
{
name: 'fill-eraser',
icon: 'fill',
fa: 'fas',
icon: 'auto_fix_off',
},
];
@ -218,7 +210,6 @@ export const maxUndoHistory = 500;
export const tabLimit = 20;
export const parseMircAscii = async (contents, filename) => {
const MIRC_MAX_COLOURS = mircColours99.length;
const mIrcColourRegex = new RegExp(/\u0003(\d{0,2})?[,]?(\d{0,2})?/, 'gu');
// The current state of the Colours
@ -239,7 +230,6 @@ export const parseMircAscii = async (contents, filename) => {
const finalAscii = {
title: filename,
current: [],
layers: [{
label: filename,
visible: true,
@ -248,7 +238,6 @@ export const parseMircAscii = async (contents, filename) => {
height: contents.split('\n').length,
}],
history: [],
// redo: [],
historyIndex: 0,
imageOverlay: {
url: null,
@ -315,14 +304,6 @@ export const parseMircAscii = async (contents, filename) => {
}
}
if (Number.isNaN(curBlock.bg)) {
delete curBlock['bg'];
}
if (Number.isNaN(curBlock.fg)) {
delete curBlock['fg'];
}
colourData.push({
code: codeData,
b: {
@ -331,19 +312,19 @@ export const parseMircAscii = async (contents, filename) => {
});
}
}
// Readjust the indexes
let indexAdjustment = 0;
// Readjust the indexes
let buildString = "";
for (let index in colourData) {
if (index === 0) {
continue;
}
for (let index in colourData) {
if (index === 0) {
continue;
colourData[index].code.index = colourData[index].code.index - indexAdjustment;
indexAdjustment = indexAdjustment + colourData[index].code[0].length;
newData[colourData[index].code.index] = colourData[index].b;
}
colourData[index].code.index = colourData[index].code.index - buildString.length;
buildString = `${buildString}${colourData[index].code[0]}`
newData[colourData[index].code.index] = colourData[index].b;
}
@ -354,12 +335,11 @@ export const parseMircAscii = async (contents, filename) => {
// If there is a colour change present at this index
// we will keep track of it
if (!isPlainText && newData[i]) {
// console.log(newData[i]);
if (newData[i].bg !== undefined && newData[i].bg !== NaN) {
if (newData[i].bg !== undefined) {
curBlock.bg = newData[i].bg;
}
if (newData[i].fg !== undefined && newData[i].fg !== NaN) {
if (newData[i].fg !== undefined) {
curBlock.fg = newData[i].fg;
}
@ -373,8 +353,6 @@ export const parseMircAscii = async (contents, filename) => {
curBlock.char = char;
finalAscii.layers[0].data[y][i] = {
...curBlock
};
@ -390,244 +368,6 @@ export const parseMircAscii = async (contents, filename) => {
JSON.stringify(finalAscii.layers),
);
// What we will see on the canvas
finalAscii.current = LZString.compressToUTF16(JSON.stringify(finalAscii.layers[0].data));
// Save ASCII to storage
store.commit('newAsciibirdMeta', finalAscii);
return true;
};
export const parseMircAsciiV2 = async (content, title) => {
const MIRC_MAX_COLOURS = mircColours99.length;
// The current state of the Colours
let curBlock = {
...emptyBlock,
};
const contents = content;
const filename = title;
// set asciiImport as the entire file contents as a string
const asciiImport = contents
.split('\u0003\u0003')
.join('\u0003')
.split('\u000F').join('')
.split('\u0003\n')
.join('\n')
.split('\u0002\u0003')
.join('\u0003');
// This will end up in the asciibirdMeta
const finalAscii = {
title: filename,
current: [],
layers: [{
label: filename,
visible: true,
data: create2DArray(asciiImport.split('\n').length),
width: false, // defined in: switch (curChar) case "\n":
height: asciiImport.split('\n').length,
}],
history: [],
// redo: [],
historyIndex: 0,
imageOverlay: {
url: null,
opacity: 95,
position: 'centered',
size: 100,
repeatx: true,
repeaty: true,
visible: false,
stretched: false,
},
x: blockWidth * 35, // the dragable ascii canvas x
y: blockHeight * 2, // the dragable ascii canvas y
selectedLayer: 0,
};
// Turn the entire ascii string into an array
let asciiStringArray = asciiImport.split('');
const linesArray = asciiImport.split('\n');
// The proper X and Y value of the block inside the ASCII
let asciiX = 0;
let asciiY = 0;
// used to determine colours
let colourChar1 = null;
let colourChar2 = null;
let parsedColour = null;
// This variable just counts the amount of colour and char codes to minus
// to get the real width
let widthOfColCodes = 0;
// Better for colourful asciis
let maxWidthLoop = 0;
// Used before the loop, better for plain text
let maxWidthFound = 0;
for (let i = 0; i < linesArray.length; i++) {
if (linesArray[i].length > maxWidthFound) {
maxWidthFound = linesArray[i].length;
}
}
while (asciiStringArray.length) {
const curChar = asciiStringArray[0];
// Defining a small finite state machine
// to detect the colour code
switch (curChar) {
case '\n':
// Reset the colours here on a new line
curBlock = {
...emptyBlock,
};
if (linesArray[asciiY] && linesArray[asciiY].length > maxWidthLoop) {
maxWidthLoop = linesArray[asciiY].length;
}
// the Y value of the ascii
asciiY++;
// Calculate widths mirc asciis vs plain text
if (!finalAscii.layers[0].width && widthOfColCodes > 0) {
finalAscii.layers[0].width = maxWidthLoop - widthOfColCodes;
}
if (!finalAscii.layers[0].width && widthOfColCodes === 0) {
// Plain text
finalAscii.layers[0].width = maxWidthFound;
}
// Resets the X value
asciiX = 0;
asciiStringArray.shift();
widthOfColCodes = 0;
break;
case '\u0003':
// Remove the colour char
asciiStringArray.shift();
widthOfColCodes++;
// Attempt to work out bg
colourChar1 = `${asciiStringArray[0]}`;
colourChar2 = `${asciiStringArray[1]}`;
parsedColour = parseInt(`${colourChar1}${colourChar2}`);
// Work out the 01, 02 double digit codes
if (parseInt(colourChar1) === 0 && parseInt(colourChar2) >= 0) {
asciiStringArray.shift();
}
if (Number.isNaN(parsedColour)) {
curBlock.bg = parseInt(colourChar1);
widthOfColCodes += 1;
asciiStringArray.shift();
} else if (parsedColour <= MIRC_MAX_COLOURS && parsedColour >= 0) {
curBlock.fg = parseInt(parsedColour);
widthOfColCodes += parsedColour.toString().length;
asciiStringArray = asciiStringArray.slice(
parsedColour.toString().length,
asciiStringArray.length,
);
}
// No background colour
if (asciiStringArray[0] !== ',') {
break;
} else {
// Remove , from array
widthOfColCodes += 1;
asciiStringArray.shift();
}
// Attempt to work out bg
colourChar1 = `${asciiStringArray[0]}`;
colourChar2 = `${asciiStringArray[1]}`;
parsedColour = parseInt(`${colourChar1}${colourChar2}`);
if (
!Number.isNaN(colourChar1) &&
!Number.isNaN(colourChar2) &&
parseInt(colourChar2) > parseInt(colourChar1) &&
!Number.isNaN(parsedColour) &&
parseInt(parsedColour) < 10
) {
parsedColour = parseInt(colourChar2);
widthOfColCodes += 1;
asciiStringArray.shift();
}
if (
parseInt(colourChar2) === parseInt(colourChar1) &&
parseInt(parsedColour) < 10
) {
parsedColour = parseInt(colourChar1);
asciiStringArray.shift();
asciiStringArray.shift();
widthOfColCodes += 2;
curBlock.bg = parseInt(colourChar1);
break;
}
if (Number.isNaN(parsedColour)) {
curBlock.bg = parseInt(colourChar1);
widthOfColCodes += 1;
asciiStringArray.shift();
} else if (parsedColour <= MIRC_MAX_COLOURS && parsedColour >= 0) {
curBlock.bg = parseInt(parsedColour);
widthOfColCodes += parsedColour.toString().length;
asciiStringArray = asciiStringArray.slice(
parsedColour.toString().length,
asciiStringArray.length,
);
break;
}
break;
default:
curBlock.char = curChar;
asciiStringArray.shift();
asciiX++;
finalAscii.layers[0].data[asciiY][asciiX - 1] = {
...curBlock,
};
break;
} // End Switch
} // End loop charPos
// First layer data generation
finalAscii.layers = [...fillNullBlocks(finalAscii.layers[0].height, finalAscii.layers[0]
.width, finalAscii.layers)];
// Store the ASCII and ensure we have no null blocks
finalAscii.layers = LZString.compressToUTF16(
JSON.stringify(finalAscii.layers),
);
// What we will see on the canvas
finalAscii.current = LZString.compressToUTF16(JSON.stringify(finalAscii.layers[0].data));
// We need to also store in the first undo history the original state
// finalAscii.history.push(finalAscii.layers);
// Save ASCII to storage
store.commit('newAsciibirdMeta', finalAscii);
@ -639,11 +379,9 @@ export const createNewAscii = (forms) => {
const newAscii = {
title: forms.createAscii.title,
history: [],
// redo: [],
historyIndex: 0,
x: 247, // the dragable ascii canvas x
y: 24, // the dragable ascii canvas y
current: [],
layers: [{
label: forms.createAscii.title,
visible: true,
@ -674,8 +412,7 @@ export const createNewAscii = (forms) => {
}
newAscii.layers = LZString.compressToUTF16(JSON.stringify(newAscii.layers));
newAscii.current = LZString.compressToUTF16(JSON.stringify(newAscii.layers[0].data));
// newAscii.history.push(newAscii.layers);
store.commit('newAsciibirdMeta', newAscii);
store.commit('closeModal', 'new-ascii');
@ -683,7 +420,7 @@ export const createNewAscii = (forms) => {
};
// Converts ASCIIBIRD blocks to mIRC colours
export const exportMirc = () => {
export const exportMirc = (blocks = null) => {
const {
currentAscii
} = store.getters;
@ -692,7 +429,10 @@ export const exportMirc = () => {
currentAsciiLayersWidthHeight
} = store.getters;
const blocks = mergeLayers();
if (blocks === null) {
blocks = mergeLayers();
}
const output = [];
let curBlock = false;
let pushString = '';

View File

@ -49,34 +49,26 @@
<t-button
type="button"
class="rounded-3xl h-7 ml-1 mt-1"
@click="saveToLibrary(decompressBlock(brush.blocks))"
>
<font-awesome-icon
:icon="['fas', 'save']"
size="lg"
class="p-1 mx-1"
/>
<span class="material-icons">save</span>
</t-button>
<t-button
type="button"
@click="reuseBlocks(decompressBlock(brush.blocks))"
>
<font-awesome-icon
:icon="['fas', 'paint-brush']"
size="lg"
class="p-1 mx-1"
/>
</t-button>
class="rounded-3xl h-7 ml-1 mt-1"
@click="reuseBlocks(decompressBlock(brush.blocks))">
<span class="material-icons">brush</span>
</t-button>
<t-button
type="button"
class="rounded-3xl h-7 ml-1 mt-1"
@click="removeFromHistory(decompressBlock(brush.blocks))"
>
<font-awesome-icon
:icon="['fas', 'trash']"
size="lg"
class="p-1 mx-1 right-auto"
/>
<span class="material-icons">delete</span>
</t-button>
</t-card>
</div>
@ -91,23 +83,19 @@
<t-button
type="button"
class="rounded-3xl h-7 ml-1 mt-1"
@click="removeFromLibrary(decompressBlock(brush.blocks))"
>
<font-awesome-icon
:icon="['fas', 'trash']"
size="lg"
class="p-1 mx-1"
/>
<span class="material-icons">trash</span>
</t-button>
<t-button
type="button"
class="rounded-3xl h-7 ml-1 mt-1"
@click="reuseBlocks(decompressBlock(brush.blocks))"
>
<font-awesome-icon
:icon="['fas', 'paint-brush']"
size="lg"
class="p-1 mx-1"
/>
<span class="material-icons">brush</span>
</t-button>
</t-card>
</div>

View File

@ -13,7 +13,7 @@
<t-button
type="button"
:style="`background-color: ${mircColours[currentBg]} !important;`"
class="border-gray-200 w-12 h-12 text-2xl ml-2"
class="border-gray-200 w-12 h-12 text-2xl ml-4"
id="currentColourBg"
@click="$store.commit('changeIsUpdatingBg', ! toolbarState.isChoosingBg )"
>
@ -22,18 +22,18 @@
<t-button
type="button"
class="bg-white absolute rounded-3xl"
style="margin-left: -62px; margin-top: 12px"
class="rounded-3xl"
style="margin-left: -68px; margin-top: 12px;"
id="swapColour"
@click="swapColours()"
>
<font-awesome-icon :icon="['fas', 'sync']" />
<span class="material-icons">swap_horiz</span>
</t-button>
<t-button
type="button"
:style="`background-color: ${mircColours[currentBg]} !important;color: ${mircColours[currentFg]};`"
class="border-gray-200 w-12 h-12 text-2xl ml-2"
class="border-gray-200 w-12 h-12 text-2xl ml-12"
id="currentChar"
@click="$store.commit('changeIsUpdatingChar', ! toolbarState.isChoosingChar )"
>

View File

@ -110,7 +110,9 @@ export default {
asciiStats() {
// const compressed = ( JSON.stringify(this.currentAscii) / 1024).toFixed(2);
// const uncompressed = (JSON.stringify(this.currentAscii).length / 1024).toFixed(2);
const stateSize = (JSON.stringify(this.state).length / 1024).toFixed(2);
const byteSize = str => new Blob([str]).size;
const stateSize = (byteSize(JSON.stringify(this.state)) / 1024).toFixed(2);
return {
stateSize: `${stateSize}kb`,

View File

@ -22,75 +22,77 @@
<div class="flex">
<label class="ml-1 w-1/3">
<t-checkbox
class="form-checkbox m-1"
class="form-checkbox h-5 w-5 text-blue-600"
name="targetingFg"
v-model="toolbarState.targetingFg"
:disabled="!canBg && !canText"
/>
<span class="text-sm">FG</span>
<span class="ab-checkbox-label">FG</span>
</label>
<label class="ml-1 w-1/3">
<t-checkbox
class="form-checkbox m-1"
class="ab-checkbox"
name="targetingBg"
v-model="toolbarState.targetingBg"
:disabled="!canFg && !canText"
checked
/>
<span class="text-sm">BG</span>
<span class="ab-checkbox-label">BG</span>
</label>
<label class="ml-1 w-1/3">
<t-checkbox
class="form-checkbox m-1"
class="ab-checkbox"
name="targetingChar"
v-model="toolbarState.targetingChar"
:disabled="!canFg && !canBg"
/>
<span class="text-sm">Text</span>
<span class="ab-checkbox-label">Text</span>
</label>
</div>
<div class="flex">
<label class="ml-1 w-1/2">
<t-checkbox
class="form-checkbox m-1"
class="ab-checkbox"
name="mirror-x"
v-model="mirror.x"
@change="updateMirror()"
/>
<span class="text-sm">Mirror X</span>
<span class="ab-checkbox-label">Mirror X</span>
</label>
<label class="ml-1 w-1/2">
<t-checkbox
class="form-checkbox m-1"
class="ab-checkbox"
name="mirror-y"
v-model="mirror.y"
@change="updateMirror()"
/>
<span class="text-sm">Mirror Y</span>
<span class="ab-checkbox-label">Mirror Y</span>
</label>
</div>
<div class="flex">
<label class="ml-1 w-1/2">
<t-checkbox
class="form-checkbox m-1"
class="ab-checkbox"
name="update-brush"
v-model="toolbarState.updateBrush"
@change="$store.commit('toggleUpdateBrush', updateBrush)"
/>
<span class="text-sm">Update Brush</span>
<span class="ab-checkbox-label">Update Brush</span>
</label>
<label class="ml-1 w-1/2">
<t-checkbox
class="form-checkbox m-1"
class="ab-checkbox"
name="grid"
v-model="toolbarState.gridView"
@change="$store.commit('toggleGridView', gridView)"
/>
<span class="text-sm">Grid</span>
<span class="ab-checkbox-label">Grid</span>
</label>
</div>
@ -107,7 +109,7 @@
}`"
@click="$store.commit('changeTool', keyToolbar)"
>
<font-awesome-icon :icon="[value.fa, value.icon]" size="lg" />
<span class="material-icons">{{ value.icon }}</span>
</t-button>
</t-card>
</vue-draggable-resizable>

View File

@ -8,8 +8,33 @@
class="previewcanvas"
:width="blocksWidthHeight.w"
:height="blocksWidthHeight.h"
@mouseup.right="openContextMenu"
@contextmenu.prevent
/>
</div>
<context-menu :ref="`block-menu-${hash}`" class="z-50" >
<ul>
<li
@click="true"
class="ml-1 text-sm hover:bg-gray-400"
>
Save as PNG
</li>
<li
@click="true"
class="ml-1 text-sm hover:bg-gray-400"
>
Export ASCII to mIRC Clipboard
</li>
<li
@click="true"
class="ml-1 text-sm hover:bg-gray-400"
>
Export ASCII to mIRC File
</li>
</ul>
</context-menu>
</t-card>
</div>
</template>
@ -17,7 +42,7 @@
<script>
import { mircColours99, blockWidth, blockHeight, cyrb53, getBlocksWidth, filterNullBlocks } from "../../ascii";
import ContextMenu from "./ContextMenu.vue";
export default {
name: "BrushCanvas",
created() {
@ -37,6 +62,9 @@ export default {
default: false,
},
},
components: {
ContextMenu,
},
data: () => ({
ctx: null,
redraw: true,
@ -87,9 +115,11 @@ export default {
options() {
return this.$store.getters.options;
},
hash() {
return cyrb53(JSON.stringify(this.getBlocks));
},
canvasName() {
let hash = cyrb53(JSON.stringify(this.getBlocks))
return `${hash}-brush-canvas`;
return `${this.hash}-brush-canvas`;
},
getBlocks() {
return this.blocks === false
@ -157,6 +187,14 @@ export default {
filterNullBlocks(blocks) {
return filterNullBlocks(blocks)
},
openContextMenu(e) {
e.preventDefault();
// These are the correct X and Y when inside the floating panel
this.$refs[`block-menu-${this.hash}`].open({
pageX: e.layerX,
pageY: e.layerY,
});
},
drawPreview() {
this.ctx.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
this.ctx.fillStyle = this.mircColours[1];

View File

@ -22,43 +22,32 @@
<div class="w-full bg-white rounded-lg shadow">
<ul class="divide-y-2 divide-gray-100 mb-2">
<div class="flex p-1">
<t-button
type="button"
class="rounded-xl"
@click="updateImageOverlay"
>
<font-awesome-icon
:icon="['fas', imageOverlay.visible ? 'eye' : 'eye-slash']"
/> </t-button
>
<div class="w-full p-1" @click="showOverlayModal">
<div class="flex text-right" >
<div class="w-full">
<t-card class="w-full pl-2 hover:bg-gray-300 cursor-pointer">
<span>{{
imageOverlayUrl || 'Image Overlay'
}}</span>
</t-card>
</div>
</div>
</div>
<div class="flex p-1">
<t-button
type="button"
class="rounded-xl"
@click="updateImageOverlay"
>
<span class="material-icons">{{
imageOverlay.visible ? "remove_red_eye" : "panorama_fish_eye"
}}</span>
</t-button>
<div class="w-full p-1" @click="showOverlayModal">
<div class="flex text-right">
<div class="w-full">
<t-card class="w-full pl-2 hover:bg-gray-300 cursor-pointer">
<span>{{ imageOverlayUrl || "Image Overlay" }}</span>
</t-card>
</div>
</div>
</div>
</div>
</ul>
<ul class="mt-1 mb-2">
<li></li>
</ul>
<ul class="divide-y-2 divide-gray-100 reverseorder">
<li
:class="`p-1 ${selectedLayerClass(key)}`"
@ -69,22 +58,22 @@
<div class="w-12">
<t-button
type="button"
class="rounded-xl"
class="rounded-3xl h-7"
@click="toggleLayer(key)"
:disabled="!canToggleLayer"
>
<font-awesome-icon
:icon="['fas', layer.visible ? 'eye' : 'eye-slash']"
/> </t-button
<span class="material-icons">{{ layer.visible ? "remove_red_eye" : "panorama_fish_eye" }}</span>
</t-button
><br />
<t-button
type="button"
class="rounded-xl"
class="rounded-3xl h-7"
@click="removeLayer(key)"
:disabled="!canToggleLayer"
>
<font-awesome-icon :icon="['fas', 'trash']" />
<span class="material-icons">delete</span>
</t-button>
</div>
@ -101,22 +90,22 @@
<div class="w-5">
<t-button
type="button"
class="rounded-xl"
class="rounded-3xl h-7"
@click="downLayer(key)"
:disabled="!canToggleLayer"
>
<font-awesome-icon
:icon="['fas', 'chevron-circle-up']"
/> </t-button
<span class="material-icons">arrow_upward</span>
</t-button
><br />
<t-button
type="button"
class="rounded-xl"
class="rounded-3xl h-7"
@click="upLayer(key)"
:disabled="!canToggleLayer"
>
<font-awesome-icon :icon="['fas', 'chevron-circle-down']" />
<span class="material-icons">arrow_downward</span>
</t-button>
</div>
</div>
@ -152,8 +141,10 @@ export default {
return this.$store.getters.imageOverlay || false;
},
imageOverlayUrl() {
return this.imageOverlay.url ? this.imageOverlay.url.split("/").pop() : '';
}
return this.imageOverlay.url
? this.imageOverlay.url.split("/").pop()
: "";
},
},
watch: {
selectedLayer() {
@ -243,16 +234,14 @@ export default {
this.$store.commit("removeLayer", key);
},
showOverlayModal() {
this.$store.commit('openModal', 'overlay');
this.$store.commit("openModal", "overlay");
},
// Image overlay
updateImageOverlay() {
let overlay = { ... this.imageOverlay }
overlay.visible = ! overlay.visible
updateImageOverlay() {
let overlay = { ...this.imageOverlay };
overlay.visible = !overlay.visible;
this.$store.commit("updateImageOverlay", overlay);
},
},
};
</script>

View File

@ -1,42 +1,84 @@
<template>
<div>
<t-card class="overflow-x-scroll h-full">
<div :style="`height: ${blocksWidthHeight.h}px;width: ${blocksWidthHeight.w}px;`">
<t-card class="overflow-x-scroll overflow-y-scroll h-full" :h="blocksWidthHeight.h">
<div
:style="`height: ${blocksWidthHeight.h}px;width: ${blocksWidthHeight.w}px;`"
@mouseup.right="openContextMenu"
>
<canvas
ref="brushcanvas"
id="brushcanvas"
class="brushcanvas"
@mousemove="canvasMouseMove"
@mouseup="disable"
@mousedown.left="addBlock"
@mousedown.right="eraseBlock"
@mousedown.left="processClick"
@mouseup.left="canTool = false"
@contextmenu.prevent
:width="blocksWidthHeight.w"
:height="blocksWidthHeight.h"
@mouseenter="disableToolbarMoving"
@mouseleave="enableToolbarMoving"
/>
<context-menu ref="main-brush-menu" class="z-50" @contextmenu.prevent>
<ul>
<li
@click="true"
class="ml-1 text-sm hover:bg-gray-400"
>
Save as PNG
</li>
<li
@click="true"
class="ml-1 text-sm hover:bg-gray-400"
>
Export Brush to mIRC Clipboard
</li>
<li
@click="true"
class="ml-1 text-sm hover:bg-gray-400"
>
Export Brush to mIRC File
</li>
<li
@click="true"
class="ml-1 text-sm hover:bg-gray-400"
>
Save to Library
</li>
</ul>
</context-menu>
</div>
</t-card>
</div>
</template>
<script>
import ContextMenu from "./ContextMenu.vue";
import {
mircColours99,
blockWidth,
blockHeight,
getBlocksWidth,
filterNullBlocks,
toolbarIcons,
emptyBlock,
} from "../../ascii";
export default {
name: "MainBrushCanvas",
components: {
ContextMenu,
},
created() {
window.addEventListener('load', () => {
// Fixes the font on load issue
this.delayRedrawCanvas();
})
window.addEventListener("load", () => {
// Fixes the font on load issue
this.delayRedrawCanvas();
});
},
mounted() {
this.ctx = this.canvasRef.getContext("2d");
@ -45,8 +87,8 @@ export default {
data: () => ({
ctx: null,
redraw: true,
erasing: false,
drawing: false,
canTool: false,
showContextMenu: true,
x: 0,
y: 0,
}),
@ -58,7 +100,7 @@ export default {
return blockHeight * this.blockSizeMultiplier;
},
blockSizeMultiplier() {
return this.$store.getters.blockSizeMultiplier
return this.$store.getters.blockSizeMultiplier;
},
currentAscii() {
return this.$store.getters.currentAscii;
@ -123,6 +165,18 @@ export default {
gridView() {
return this.toolbarState.gridView;
},
currentTool() {
return toolbarIcons[this.$store.getters.currentTool] ?? null;
},
isDefault() {
return this.currentTool.name === "default";
},
isBrushing() {
return this.currentTool.name === "brush";
},
isErasing() {
return this.currentTool.name === "eraser";
},
},
watch: {
brushBlocks() {
@ -165,6 +219,36 @@ export default {
},
},
methods: {
openContextMenu(e) {
e.preventDefault();
// These are the correct X and Y when inside the floating panel
this.$refs['main-brush-menu'].open({
pageX: e.layerX,
pageY: e.layerY,
});
},
processClick(e) {
this.canTool = true;
if (e.offsetX >= 0) {
this.x = e.offsetX;
}
if (e.offsetY >= 0) {
this.y = e.offsetY;
}
this.x = Math.floor(this.x / blockWidth);
this.y = Math.floor(this.y / blockHeight);
if (this.isErasing) {
this.eraseBlock();
}
if (this.isBrushing) {
this.addBlock();
}
},
getBlocksWidth(blocks) {
return getBlocksWidth(blocks);
},
@ -182,19 +266,19 @@ export default {
ctx.moveTo(x, 0);
ctx.lineTo(x, h);
}
ctx.strokeStyle = "rgba(0, 0, 0, 1)";
ctx.lineWidth = 1;
ctx.setLineDash([1]);
ctx.stroke();
ctx.beginPath();
for (var y = 0; y <= h; y += blockHeight) {
ctx.moveTo(0, y);
ctx.lineTo(w, y);
}
ctx.stroke();
},
@ -216,7 +300,7 @@ export default {
if (this.brushBlocks[y] && this.brushBlocks[y][x]) {
const curBlock = this.brushBlocks[y][x];
if ((curBlock.bg !== undefined || curBlock.bg !== null) && this.isTargettingBg) {
if (curBlock.bg !== undefined) { // we had used to hide or show depending on canFg, etc && this.isTargettingBg
this.ctx.fillStyle = this.mircColours[curBlock.bg];
this.ctx.fillRect(
@ -227,11 +311,11 @@ export default {
);
}
if (curBlock.fg !== undefined && this.isTargettingFg) {
if (curBlock.fg !== undefined) { // we had used to hide or show depending on canFg, etc && this.isTargettingFg
this.ctx.fillStyle = this.mircColours[curBlock.fg];
}
if (curBlock.char !== undefined && this.isTargettingChar) {
if (curBlock.char !== undefined) { // we had used to hide or show depending on canFg, etc && this.isTargettingChar
this.ctx.fillStyle = this.mircColours[curBlock.fg];
this.ctx.fillText(
curBlock.char,
@ -243,84 +327,68 @@ export default {
}
}
if (this.gridView) {
this.drawGrid();
}
}
},
delayRedrawCanvas() {
if (this.redraw) {
this.redraw = false;
var _this = this;
setTimeout(function(){
setTimeout(function () {
requestAnimationFrame(() => {
_this.drawPreview();
_this.redraw = true;
});
}, 1000/this.options.fps)
}, 1000 / this.options.fps);
}
},
// Basic block editing
canvasMouseMove(e) {
if (e.offsetX >= 0) {
this.x = e.offsetX;
if (this.canTool) {
this.processClick(e);
}
},
addBlock() {
let block = { ...emptyBlock };
if (this.canBg) {
block["bg"] = this.currentBg;
}
if (e.offsetY >= 0) {
this.y = e.offsetY;
if (this.canFg) {
block["fg"] = this.currentFg;
}
this.x = Math.floor(this.x / blockWidth);
this.y = Math.floor(this.y / blockHeight);
if (this.erasing) {
this.brushBlocks[this.y][this.x] = {
bg: this.canBg ? null : this.currentFg,
fg: this.canFg ? null : this.currentBg,
char: this.canText ? null : this.currentChar,
};
if (this.canText) {
block["char"] = this.currentChar;
}
if (this.drawing) {
this.brushBlocks[this.y][this.x] = {
bg: this.canBg ? this.currentBg : null,
fg: this.canFg ? this.currentFg : null,
char: this.canText ? this.currentChar : null,
};
this.brushBlocks[this.y][this.x] = block;
this.delayRedrawCanvas();
},
eraseBlock() {
if (this.canBg) {
delete this.brushBlocks[this.y][this.x]["bg"];
}
if (this.canFg) {
delete this.brushBlocks[this.y][this.x]["fg"];
}
if (this.canText) {
delete this.brushBlocks[this.y][this.x]["char"];
}
this.delayRedrawCanvas();
},
disable() {
this.erasing = false;
this.drawing = false;
},
addBlock() {
this.drawing = true;
this.brushBlocks[this.y][this.x] = {
bg: this.canBg ? this.currentBg : null,
fg: this.canFg ? this.currentFg : null,
char: this.canText ? this.currentChar : null,
};
},
eraseBlock(e) {
this.erasing = true;
this.brushBlocks[this.y][this.x] = {
bg: this.canBg ? null : this.currentFg,
fg: this.canFg ? null : this.currentBg,
char: this.canText ? null : this.currentChar,
};
},
disableToolbarMoving() {
this.$store.commit("changeToolBarDraggable", false);
},
enableToolbarMoving() {
// Save the blocks when the mouse leaves the canvas area
// To avoid one block history changes
this.disable()
this.$store.commit("brushBlocks", this.brushBlocks);
this.$store.commit("changeToolBarDraggable", true);
},

View File

@ -10,14 +10,8 @@ import Dashboard from './Dashboard.vue';
import Toasted from 'vue-toasted';
Vue.config.productionTip = false;
import {
FontAwesomeIcon,
} from '@fortawesome/vue-fontawesome';
import './fontAwesome';
Vue.use(VueTailwind, tailwindCss);
Vue.component('font-awesome-icon', FontAwesomeIcon);
Vue.component('vue-draggable-resizable', VueDraggableResizable);
Vue.use(VueClipboard);
Vue.use(Toasted, {

View File

@ -219,7 +219,7 @@ export default new Vuex.Store({
state.asciibirdMeta[state.tab].layers = LZString.compressToUTF16(JSON.stringify(
tempLayers));
state.asciibirdMeta[state.tab].current = LZString.compressToUTF16(JSON.stringify(mergeLayers()));
// state.asciibirdMeta[state.tab].current = LZString.compressToUTF16(JSON.stringify(mergeLayers()));
let historyIndex = state.asciibirdMeta[state.tab].historyIndex;
@ -683,7 +683,6 @@ export default new Vuex.Store({
selectedLayer: (state) => state.asciibirdMeta[state.tab].selectedLayer,
imageOverlay: (state) => state.asciibirdMeta[state.tab].imageOverlay,
asciibirdMeta: (state) => state.asciibirdMeta,
nextTabValue: (state) => state.asciibirdMeta.length,
brushSizeHeight: (state) => state.toolbarState.brushSizeHeight,
brushSizeWidth: (state) => state.toolbarState.brushSizeWidth,
brushSizeType: (state) => state.toolbarState.brushSizeType,

View File

@ -15,8 +15,6 @@
font-display: swap;
}
/* @import url('https://use.fontawesome.com/releases/v5.0.9/css/all.css'); */
body {
background: rgb(58, 58, 58);
-ms-user-select: none;
@ -25,7 +23,7 @@ body {
-webkit-touch-callout: none;
-khtml-user-select: none;
user-select: none;
overflow-y: hidden;
/* overflow-y: hidden; */
}
.canvas {
@ -83,4 +81,19 @@ body {
@apply bg-gray-800;
}
.ab-toolbar-button {
@apply w-10 h-10 mt-1 ml-1 border-gray-200 bg-gray-500;
}
.ab-toolbar-button-active {
@apply w-10 h-10 mt-1 ml-1 border-gray-900 bg-blue-500;
}
.ab-checkbox {
@apply h-4 w-4 text-blue-600;
}
.ab-checkbox-label {
@apply ml-2 text-gray-700;
}
}

View File

@ -5,6 +5,30 @@
@mouseleave="isMouseOnCanvas = false"
@mouseenter="isMouseOnCanvas = true"
>
<context-menu ref="editor-menu" class="z-50" >
<ul>
<li
@click="true"
class="ml-1 text-sm hover:bg-gray-400"
>
Save as PNG
</li>
<li
@click="true"
class="ml-1 text-sm hover:bg-gray-400"
>
Export ASCII to mIRC Clipboard
</li>
<li
@click="true"
class="ml-1 text-sm hover:bg-gray-400"
>
Export ASCII to mIRC File
</li>
</ul>
</context-menu>
<vue-draggable-resizable
ref="canvasdrag"
:grid="[blockWidth, blockHeight]"
@ -41,6 +65,8 @@
@mousemove.left="canvasMouseMove"
@mousedown.left="canvasMouseDown"
@mouseup.left="canvasMouseUp"
@mouseup.right="openContextMenu"
@contextmenu.prevent
@touchmove="canvasMouseMove"
@touchend="canvasMouseDown"
@touchstart="canvasMouseUp"
@ -51,6 +77,7 @@
</template>
<script>
import ContextMenu from "./../components/parts/ContextMenu.vue";
import {
toolbarIcons,
mircColours99,
@ -62,11 +89,13 @@ import {
getBlocksWidth,
checkVisible,
mergeLayers,
cyrb53,
} from "../ascii";
export default {
name: "Editor",
components: {
ContextMenu,
},
mounted() {
this.ctx = this.canvasRef.getContext("2d");
this.toolCtx = this.$refs.canvastools.getContext("2d");
@ -297,7 +326,9 @@ export default {
return this.currentSelectedLayer.width;
},
currentAsciiHeight() {
return this.currentSelectedLayer.height;
// Tested with wtf.txt and the max rows before the canvas
// stops working seems to be 2184
return (this.currentSelectedLayer.height > 2184 ? 2184 : this.currentSelectedLayer.height);
},
imageOverlay() {
return this.$store.getters.imageOverlay;
@ -429,6 +460,11 @@ export default {
}
},
methods: {
openContextMenu(e) {
e.preventDefault();
// These are the correct X and Y when inside the floating panel
this.$refs['editor-menu'].open(e);
},
canvasKeyDown(char) {
// if (this.isTextEditing) {
console.log(char);
@ -754,35 +790,38 @@ export default {
return mergeLayers();
},
drawGrid() {
let ctx = this.ctx;
let w = this.canvas.width;
let h = this.canvas.height;
ctx.beginPath();
this.ctx.beginPath();
for (var x = 0; x <= w; x += blockWidth) {
ctx.moveTo(x, 0);
ctx.lineTo(x, h);
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, h);
}
ctx.strokeStyle = "rgba(0, 0, 0, 1)";
ctx.lineWidth = 1;
ctx.setLineDash([1]);
this.ctx.strokeStyle = "rgba(0, 0, 0, 1)";
this.ctx.lineWidth = 1;
this.ctx.setLineDash([1]);
ctx.stroke();
this.ctx.stroke();
ctx.beginPath();
this.ctx.beginPath();
for (var y = 0; y <= h; y += blockHeight) {
ctx.moveTo(0, y);
ctx.lineTo(w, y);
this.ctx.moveTo(0, y);
this.ctx.lineTo(w, y);
}
ctx.stroke();
this.ctx.stroke();
},
redrawCanvas() {
if (this.currentAsciiLayers.length) {
// https://stackoverflow.com/questions/28390358/high-cpu-usage-with-canvas-and-requestanimationframe
this.ctx.save();
this.canvasRef.width = this.canvasRef.width;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Position of the meta array
let x = 0;
let y = 0;
@ -838,6 +877,8 @@ export default {
if (this.gridView) {
this.drawGrid();
}
this.ctx.restore();
}
},
onCanvasResize(left, top, width, height) {
@ -1066,6 +1107,8 @@ export default {
clearToolCanvas() {
if (this.toolCtx) {
this.toolCtx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.toolCtx.save();
this.$refs.canvastools.width = this.$refs.canvastools.width;
}
},
delayRedrawCanvas() {
@ -1335,8 +1378,8 @@ export default {
}
// Apply text to ascii blocks
if (this.canText && this.canTool && brushBlock.char !== null) {
targetBlock.char = brushBlock.char;
if (this.canText && this.canTool) {
targetBlock['char'] = brushBlock['char'];
if (
this.mirrorX &&
@ -1433,6 +1476,7 @@ export default {
// Apply the actual brush block to the ascii block
if (this.canTool && brushBlock[target] !== undefined) {
targetBlock[target] = brushBlock[target];
// targetBlock['char'] = brushBlock['char'];
let theX = asciiWidth - arrayX;
let theY = asciiHeight - arrayY;
@ -1473,6 +1517,7 @@ export default {
}
}
this.toolCtx.restore();
return;
},
//
@ -1483,12 +1528,12 @@ export default {
drawBrush(plain = false) {
this.clearToolCanvas();
let brushDiffX = 0;
let xLength = 0;
let xLength = false;
// If the first row isn't selected then we cannot get the width
// with the 0 index
for (let i = 0; i <= this.brushBlocks.length; i++) {
if (this.brushBlocks[i]) {
if (this.brushBlocks[i] && xLength === false) {
brushDiffX = Math.floor(this.brushBlocks[i].length / 2) * blockWidth;
xLength = this.brushBlocks[i].length;
break;
@ -1507,6 +1552,7 @@ export default {
continue;
}
// if (
// this.top !== false &&
// !this.checkVisible(this.top + (y * blockHeight) - this.yOffset)
@ -1540,7 +1586,9 @@ export default {
}
this.drawBrushBlocks(brushX, brushY, brushBlock, null);
} else {
}
else if (this.isErasing) {
this.drawBrushBlocks(brushX, brushY, brushBlock, null, true);
}

View File

@ -903,37 +903,6 @@
"@babel/helper-validator-identifier" "^7.14.9"
to-fast-properties "^2.0.0"
"@fortawesome/fontawesome-common-types@^0.2.36":
version "0.2.36"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz#b44e52db3b6b20523e0c57ef8c42d315532cb903"
integrity sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==
"@fortawesome/fontawesome-svg-core@^1.2.35":
version "1.2.36"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz#4f2ea6f778298e0c47c6524ce2e7fd58eb6930e3"
integrity sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.36"
"@fortawesome/free-regular-svg-icons@^5.15.3":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz#b97edab436954333bbeac09cfc40c6a951081a02"
integrity sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.36"
"@fortawesome/free-solid-svg-icons@^5.15.3":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz#2a68f3fc3ddda12e52645654142b9e4e8fbb6cc5"
integrity sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.36"
"@fortawesome/vue-fontawesome@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-2.0.2.tgz#5b86cd2fb7b4c17e5dede722c1c2855c97eceaea"
integrity sha512-ecpKSBUWXsxRJVi/dbOds4tkKwEcBQ1JSDZFzE2jTFpF8xIh3OgTX8POIor6bOltjibr3cdEyvnDjecMwUmxhQ==
"@hapi/address@2.x.x":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"