AnimationTool/app/page.ts

373 lines
10 KiB
TypeScript
Raw Permalink Normal View History

2019-09-28 02:29:42 +00:00
import { saveAs } from 'file-saver';
import * as JSZip from 'jszip';
2019-09-26 01:59:16 +00:00
import { CanvasHandler } from './canvas_handler';
2019-09-25 03:16:07 +00:00
import { FileHandler } from './file_handler';
import { FrameHandler } from './frame_handler';
2019-09-26 01:59:16 +00:00
import { IAnimationData } from './Interfaces/IAnimationData';
import { IFrame } from './Interfaces/IFrame';
2019-10-01 04:58:04 +00:00
import { IProjectData } from './Interfaces/IProjectData';
2019-09-28 17:59:50 +00:00
import { PinHandler } from './pin_handler';
2019-09-25 03:16:07 +00:00
2019-09-25 00:01:54 +00:00
export class Page {
2019-09-25 03:16:07 +00:00
private static handleDragOver(evt: DragEvent) {
if (evt !== null) {
evt.stopPropagation();
evt.preventDefault();
evt.dataTransfer!.dropEffect = 'copy'; // Explicitly show this is a copy.
}
}
2019-09-28 17:59:50 +00:00
private pinHandler: PinHandler;
2019-09-26 01:59:16 +00:00
private frameHandler: FrameHandler;
private canvasHandler: CanvasHandler;
private animationData: IAnimationData;
2019-09-27 01:07:41 +00:00
private frameRateInput: HTMLInputElement;
2019-09-27 02:02:46 +00:00
private loopingInput: HTMLInputElement;
private canvasImage: HTMLCanvasElement;
private canvasContext: CanvasRenderingContext2DSettings;
2019-09-25 03:16:07 +00:00
private projectData: IProjectData;
2019-09-28 00:04:01 +00:00
private filenameInput: HTMLInputElement;
2019-09-27 23:45:52 +00:00
2019-10-03 23:33:32 +00:00
private message: HTMLElement;
2019-10-07 17:51:43 +00:00
private addPinButton: HTMLElement;
2019-10-07 23:52:34 +00:00
private outputMessage: HTMLElement;
private canvasMouseHeld: boolean = false;
2019-09-25 00:01:54 +00:00
public Load() {
2019-09-26 01:59:16 +00:00
// defining blank slate animation data
this.animationData = {
frameRate: 30,
frames: [
{
2019-10-09 01:50:03 +00:00
filename: '',
2019-12-12 20:09:00 +00:00
pinData: []
2019-09-26 01:59:16 +00:00
}
2019-10-07 17:51:43 +00:00
],
loop: true,
originX: -1,
originY: -1,
2019-12-12 21:17:32 +00:00
pinDefinitions: []
2019-09-26 01:59:16 +00:00
};
2019-09-28 02:29:42 +00:00
// blank slate canvas data
this.projectData = {
currentFrame: 0,
2019-12-12 21:17:32 +00:00
currentlySelectedPin: -1,
2019-09-27 23:45:52 +00:00
height: 0,
2019-10-07 17:51:43 +00:00
heightRatio: 0,
width: 0,
widthRatio: 0
2019-09-27 23:45:52 +00:00
};
2019-09-26 01:59:16 +00:00
2019-10-08 22:41:37 +00:00
const info = document.getElementById('info') as HTMLElement;
const helpButton = document.getElementById('helpButton') as HTMLElement;
helpButton.addEventListener('click', () => {
info.classList.toggle('hidden');
});
const exportButton = document.getElementById('exportButton') as HTMLElement;
exportButton.addEventListener('click', () => {
this.ExportData();
});
2019-10-07 23:52:34 +00:00
this.outputMessage = document.getElementById('outputMessage') as HTMLElement;
2019-10-03 23:33:32 +00:00
this.message = document.getElementById('message') as HTMLElement;
2019-10-07 17:51:43 +00:00
const canvasImage = document.getElementById('canvasImage') as HTMLCanvasElement;
2019-09-27 02:02:46 +00:00
2019-09-27 23:45:52 +00:00
const imageElement = new Image();
2019-09-27 02:02:46 +00:00
2019-09-28 17:59:50 +00:00
this.pinHandler = new PinHandler(
document.getElementById('pinSettings') as HTMLElement,
document.getElementById('pinContainer') as HTMLElement,
document.getElementById('originPin') as HTMLElement,
this.projectData,
this.animationData
2019-09-28 17:59:50 +00:00
);
2019-10-07 17:51:43 +00:00
this.addPinButton = document.getElementById('addpin') as HTMLElement;
this.addPinButton.addEventListener('click', this.AddPinButtonPressed);
2019-09-28 17:59:50 +00:00
2019-09-27 23:45:52 +00:00
// setup canvas
this.canvasHandler = new CanvasHandler(
this.animationData,
this.projectData,
2019-10-07 17:51:43 +00:00
canvasImage,
2019-09-27 23:45:52 +00:00
imageElement,
2019-10-05 02:31:21 +00:00
document.getElementById('originInfo') as HTMLElement
2019-09-27 23:45:52 +00:00
);
2019-10-07 23:52:34 +00:00
canvasImage.addEventListener('mousedown', (event: MouseEvent) => {
this.canvasMouseHeld = true;
this.canvasHandler.CanvasMouseDown(event.offsetX, event.offsetY);
this.pinHandler.UpdatePinBoxStatus();
});
canvasImage.addEventListener('mousemove', this.CanvasMouseDown);
// reset holds on global mouse up
document.addEventListener('mouseup', () => {
this.canvasMouseHeld = false;
this.frameHandler.frameViewerMouseHeld = false;
});
2019-09-26 01:59:16 +00:00
2019-09-27 23:45:52 +00:00
// setup frame handler
2019-09-26 01:59:16 +00:00
this.frameHandler = new FrameHandler(
2019-09-27 00:45:28 +00:00
this.animationData,
this.projectData,
2019-10-07 17:51:43 +00:00
canvasImage,
canvasImage.getContext('2d')!,
2019-09-27 23:45:52 +00:00
document.getElementById('frameNumber') as HTMLElement,
imageElement,
2019-10-04 01:02:08 +00:00
this.projectData,
document.getElementById('frameViewer') as HTMLElement
2019-09-26 01:59:16 +00:00
);
2019-09-25 03:16:07 +00:00
2019-09-27 23:45:52 +00:00
// input elements
2019-09-27 01:07:41 +00:00
this.frameRateInput = document.getElementById('framerate') as HTMLInputElement;
this.frameRateInput.addEventListener('change', this.UpdateFrameRate);
2019-09-27 02:02:46 +00:00
this.loopingInput = document.getElementById('looping') as HTMLInputElement;
this.loopingInput.addEventListener('change', this.UpdateLooping);
2019-09-28 00:04:01 +00:00
this.filenameInput = document.getElementById('filename') as HTMLInputElement;
2019-09-27 01:07:41 +00:00
2019-09-25 03:16:07 +00:00
const dropZone = document.getElementById('dropZone') as HTMLElement;
dropZone.addEventListener('dragover', Page.handleDragOver, false);
dropZone.addEventListener('drop', this.handleFileSelect, false);
2019-09-25 00:42:35 +00:00
this.ResetProgram();
2019-09-25 03:16:07 +00:00
const keyDown = (event: KeyboardEvent) => {
switch (event.keyCode) {
2019-09-27 00:45:28 +00:00
case 48:
case 49:
case 50:
case 51:
case 52:
case 53:
case 54:
case 55:
case 56:
case 57: {
// goto frame w 1234567890
2019-10-04 01:11:26 +00:00
let targetFrame: number = event.keyCode - 49;
2019-09-27 00:45:28 +00:00
if (event.keyCode === 48) {
2019-10-04 01:11:26 +00:00
targetFrame = 9;
2019-09-27 00:45:28 +00:00
}
2019-10-04 01:11:26 +00:00
targetFrame %= this.animationData.frames.length;
2019-09-27 00:45:28 +00:00
this.frameHandler.StopPlayingAnimation();
2019-10-04 01:11:26 +00:00
this.frameHandler.GoToFrame(targetFrame);
this.frameHandler.RefreshFrameViewer();
2019-09-27 00:45:28 +00:00
break;
}
2019-09-26 23:28:35 +00:00
case 39:
case 190: {
// right_arrow, carrot
2019-09-26 01:59:16 +00:00
this.frameHandler.AdvanceFrames(1);
2019-09-27 00:45:28 +00:00
this.frameHandler.StopPlayingAnimation();
2019-09-25 03:16:07 +00:00
break;
}
2019-09-26 23:28:35 +00:00
case 37:
case 188: {
// left arrow, carrot
2019-09-26 01:59:16 +00:00
this.frameHandler.AdvanceFrames(-1);
2019-09-27 00:45:28 +00:00
this.frameHandler.StopPlayingAnimation();
2019-09-25 03:16:07 +00:00
break;
}
2019-09-26 23:28:35 +00:00
case 40: {
// down arrow
this.frameHandler.GoToFrame(0);
2019-09-27 00:45:28 +00:00
this.frameHandler.StopPlayingAnimation();
break;
2019-09-26 23:28:35 +00:00
}
case 32: {
// spacebar
this.frameHandler.TogglePlayingAnimation();
2019-09-27 00:45:28 +00:00
break;
}
case 83: {
if (document.activeElement === document.body) {
this.ExportData();
2019-09-28 00:04:01 +00:00
}
2019-09-26 23:28:35 +00:00
}
2019-09-25 03:16:07 +00:00
}
2019-09-25 00:42:35 +00:00
};
2019-09-25 03:16:07 +00:00
document.addEventListener('keydown', keyDown);
2019-09-25 00:01:54 +00:00
}
2019-09-25 03:16:07 +00:00
private ExportData() {
2019-12-12 21:17:32 +00:00
this.pinHandler.UpdateAnimationPinDefinitions();
if (this.ProjectHasNeccesaryData()) {
const zip = new JSZip();
// name of project
const name = this.filenameInput.value;
2019-10-09 01:50:03 +00:00
// generate output filenames
var outputFilenames = [];
for (let i = 0; i < this.animationData.frames.length; i++) {
const padding = i.toString().padStart(3, '0');
const filename = name + '_' + padding.toString() + '.png';
outputFilenames[i] = filename;
}
for (let i = 0; i < this.animationData.frames.length; i++) {
this.animationData.frames[i].filename = outputFilenames[i];
}
// .anim file
zip.file(name + '.anim', JSON.stringify(this.animationData));
2019-10-09 01:50:03 +00:00
// pngs
const filenames = this.frameHandler.GetFilenames();
for (let i = 0; i < filenames.length; i++) {
const filedata = filenames[i].split('base64,')[1];
2019-10-09 01:50:03 +00:00
zip.file(outputFilenames[i], filedata, { base64: true });
}
2019-10-09 01:50:03 +00:00
// save zip
zip.generateAsync({ type: 'blob' }).then((content) => {
// see FileSaver.js
saveAs(content, name + '.zip');
});
}
}
2019-10-04 01:02:08 +00:00
private ProjectHasNeccesaryData(): boolean {
2019-10-07 23:52:34 +00:00
this.outputMessage.innerText = '';
this.outputMessage.classList.remove('errorMessage');
2019-10-04 01:02:08 +00:00
let pass: boolean = true;
2019-10-03 23:33:32 +00:00
let errorString: string = '';
2019-10-04 01:02:08 +00:00
this.frameHandler.RefreshFrameViewer();
if (this.filenameInput.value === '') {
errorString += '- Missing name\n';
pass = false;
}
if (this.animationData.originX === null || this.animationData.originY === null) {
errorString += '- Missing origin data\n';
pass = false;
}
// check frames for data errors
let pinDataErrorString: string = '';
let passPinData: boolean = true;
for (let f = 0; f < this.animationData.frames.length; f++) {
2019-10-07 17:51:43 +00:00
const errorOnFrame: boolean = false;
2019-12-12 21:17:32 +00:00
if (this.animationData.pinDefinitions !== undefined) {
for (let p = 0; p < this.animationData.pinDefinitions.length; p++) {
if (this.animationData.pinDefinitions[p] !== undefined) {
const pinIDtoCheck = this.animationData.pinDefinitions[p].id;
2019-10-07 17:51:43 +00:00
// console.log('checking frame ' + f + ' for pinID ' + this.animationData.pins[p].name);
2019-10-09 01:50:03 +00:00
if (this.animationData.frames[f].pinData[pinIDtoCheck] === undefined) {
2019-10-04 01:02:08 +00:00
if (!errorOnFrame) {
2019-10-08 22:15:49 +00:00
pinDataErrorString += f + ' :\n';
2019-10-04 01:02:08 +00:00
}
2019-12-12 21:17:32 +00:00
pinDataErrorString += ' Pin: ' + this.animationData.pinDefinitions[p].name + '\n';
2019-10-04 01:02:08 +00:00
passPinData = false;
}
}
2019-10-03 23:33:32 +00:00
}
}
}
2019-10-04 01:02:08 +00:00
if (!passPinData) {
errorString += '- Missing pin data on some frames: \n' + pinDataErrorString;
pass = false;
2019-10-03 23:33:32 +00:00
}
2019-10-04 01:02:08 +00:00
if (!pass) {
2019-10-07 23:52:34 +00:00
this.outputMessage.innerText = errorString;
this.outputMessage.classList.add('errorMessage');
2019-10-03 23:33:32 +00:00
}
2019-10-04 01:02:08 +00:00
return pass;
}
2019-10-07 17:51:43 +00:00
private CanvasMouseDown = (event: MouseEvent) => {
2019-10-07 23:52:34 +00:00
if (this.canvasMouseHeld) {
this.canvasHandler.CanvasMouseDown(event.offsetX, event.offsetY);
this.pinHandler.UpdatePinBoxStatus();
}
2019-10-07 17:51:43 +00:00
};
private AddPinButtonPressed = () => {
this.pinHandler.AddNewPin();
};
2019-09-26 01:59:16 +00:00
private handleFileSelect = async (event: DragEvent) => {
this.ResetProgram();
2019-09-25 03:16:07 +00:00
event.stopPropagation();
event.preventDefault();
const [ processedFilenames, originalFilenames ] = await FileHandler.ProcessImages(event.dataTransfer!.files);
this.frameHandler.loadFrames(processedFilenames);
2019-09-26 01:59:16 +00:00
const newFrames: IFrame[] = [];
for (let i = 0; i < originalFilenames.length; i++) {
2019-09-26 01:59:16 +00:00
newFrames.push({
2019-10-09 01:50:03 +00:00
filename: originalFilenames[i].toString(),
2019-12-12 20:09:00 +00:00
pinData: []
2019-09-26 01:59:16 +00:00
});
}
2019-09-25 03:16:07 +00:00
2019-09-26 01:59:16 +00:00
this.animationData.frames = newFrames;
2019-09-27 02:02:46 +00:00
this.frameHandler.GoToFrame(0);
this.frameHandler.StopPlayingAnimation();
this.frameHandler.TogglePlayingAnimation();
2019-09-28 01:35:27 +00:00
2019-10-08 03:44:23 +00:00
const imageElement = new Image();
imageElement.src = processedFilenames[0];
2019-10-08 03:44:23 +00:00
imageElement.onload = () => {
this.canvasHandler.ResizeCanvas(imageElement.width, imageElement.height);
};
2019-09-28 01:35:27 +00:00
2019-10-04 01:02:08 +00:00
this.frameHandler.ConstructFrameUI();
2019-09-25 03:16:07 +00:00
};
2019-09-27 00:45:28 +00:00
private ResetProgram = () => {
// defining blank slate animation data
2019-12-12 21:17:32 +00:00
this.animationData.pinDefinitions = [];
2019-10-04 01:02:08 +00:00
this.animationData.originX = null;
this.animationData.originY = null;
this.animationData.frameRate = 30;
this.animationData.loop = true;
2019-12-12 20:09:00 +00:00
this.animationData.frames = [ { filename: '', pinData: [] } ];
2019-09-27 00:45:28 +00:00
// blank slate canvas data
this.projectData.currentFrame = 0;
this.projectData.currentlySelectedPin = 0;
this.projectData.width = 0;
this.projectData.widthRatio = 0;
this.projectData.height = 0;
this.projectData.heightRatio = 0;
// reset input displays
this.frameRateInput.value = this.animationData.frameRate.toString();
this.loopingInput.checked = this.animationData.loop;
this.filenameInput.value = '';
2019-09-27 00:45:28 +00:00
// destroy pin divs
this.pinHandler.RemoveAllPins();
this.projectData.currentlySelectedPin = 0;
};
2019-09-27 00:45:28 +00:00
private UpdateLooping = () => {
this.animationData.loop = this.loopingInput.checked;
};
2019-09-27 01:07:41 +00:00
private UpdateFrameRate = () => {
2019-09-27 01:07:41 +00:00
this.animationData.frameRate = this.frameRateInput.valueAsNumber;
2019-09-27 23:45:52 +00:00
this.frameHandler.StopPlayingAnimation();
this.frameHandler.TogglePlayingAnimation();
2019-09-27 01:07:41 +00:00
};
2019-09-25 00:01:54 +00:00
}