Compare commits

...

3 Commits

Author SHA1 Message Date
Sascha Nitsch d97dbf9da7 updated README 2022-01-19 18:46:22 +01:00
Sascha Nitsch 613092f3ee fixed bit masking
changed distance calculation
2022-01-19 18:35:25 +01:00
Sascha Nitsch e3b55ac4fa initial javascript version 2022-01-19 18:34:37 +01:00
18 changed files with 7361 additions and 28 deletions

4
.gitignore vendored
View File

@ -3,3 +3,7 @@ build
.project
.settings/
c++/html
inbrowser/.tscache
inbrowser/jsdoc
inbrowser/node_modules
inbrowser/tmp

View File

@ -1,4 +1,31 @@
# Path animator
Source code for the Path animator framework on [the project page](https://contentnation.net/en/grumpydevelop/pathanimator).
Initial implementation in C++ running on Linux, testing needs to be done if it can be in-browser via Javascript to make it cross plattform client only.
Source code for the Path animator framework on [the project page](https://contentnation.net/en/grumpydevelop/pathanimator).
The Javascript version is ready to use in all major browser.
Just load the inbrowser/webroot/inde.html and go on. Completely client side, no server nor internet needed.
## Algorithm descriptions
## Distance Path
This algorithm uses a simple distance calculation based from one or more start points.
1. Initialization
* The start points are added to the "to process" queue.
* The distance values on all pixels are set to max distance on start.
2. Main loop
* Each point in the "to process" queue is taken and it's value (distance).
* Each surrounding pixel takes the last distance (the center) and adds the distance given by the parameters m_weighBlock and m_weightPath
The input image selects the weight from the two parameters, a 0 (black) pixel takes 100% of the block value, a (255) white pixel
is 100% path weight.
* If this distance is smaller than the previous pixel that was calulated, replace it and add the pixel to the next list of pixel to be processed.
* After the iteration is done, clear the process queue and swap with the next queue and repeat until the next queue is empty or the hard limit of 10.000 iterations is met.
3. Postprocessing (C++ version)
* After the raw distances are calculated, the actual max distance is found.
* The raw map (floating ponts) is mapped down to a 16 bit image and the distance is saved in the red channel.
* the iteration where the pixel was set is in the green channel
* the blue channel is a mask if the pixel was seen as a path (65535) or as a block (0).
The threshold value sets the point where a path becomes a block.
Setting the threshold allows some cheating. You can add an path between areas with a gray value, the algorithm uses it to calculate
distance and as a path to other ares, but shows it as a block in the blue channel.
3b. Postprocessing (JavaScript version)
* Similar to the C++ version, due to lack of 16 bit image support the higher bits are mapped to the red channel, the lower bits to the green channel.
* No iterations are exported

View File

@ -1,22 +0,0 @@
# Algrithm descriptions
## Distance Path
This algorithm uses a simple distance calculation based from one or more start points.
1. Initialization
* The start points are added to the "to process" queue.
* The distance values on all pixels are set to m_maxDistance on start.
2. Main loop
* Each point in the "to process" queue is taken and it's value (distance).
* Each surrounding pixel takes the last distance (the center) and adds the distance given by the parameters m_weighBlock and m_weightPath.
The input image selects the weight from the two parameters, a 0 (black) pixel takes 100% of the block value, a (255) white pixel
is 100% path weight.
* If this distance is smaller than the previous pixel that was calulated, replace it and add the pixel to the next list of pixel to be processed.
* After the iteration is done, clear the process queue and swap with the next queue and repeat until the next queue is empty or the hard limit of 10.000 iterations is met.
3. Postprocessing
* After the raw distances are calculated, the actual max distance is found.
* The raw map (floating ponts) is mapped down to a 16 bit image and the distance is saved in the red channel,
* the iteration where the pixel was set is in the green channel
* the blue channel is a mask if the pixel was seen as a path (65535) or as a block (0).
The m_threshhold value sets the point where a path becomes a block.
Setting the threshhold allows some cheating. You can add an path between areas with a gray value, the algorithm uses it to calculate
distance and as a path to other ares, but shows it as a block in the blue channel.

View File

@ -35,8 +35,8 @@ DistancePath::~DistancePath() {
inline void DistancePath::processPixel(uint8_t* inRAW, float* outRow, uint16_t x, uint16_t y, float lastValue, uint16_t width, std::list<uint32_t>* next, uint16_t* outPixels, uint16_t iteration) {
float *oR = &outRow[x];
uint32_t offset = (y * width + x) * 3;
float avg = 255 - (0.3333 * (inRAW[offset] + inRAW[offset + 1] + inRAW[offset + 2]));
float newValue = lastValue + avg * m_weightBlock + (255.0 - avg) * m_weightPath;
float avg = (inRAW[offset] + inRAW[offset + 1] + inRAW[offset + 2]) / 765.0;
float newValue = lastValue + (1.0 - avg) * (m_weightBlock - m_weightPath) + m_weightPath;
if (newValue > m_maxDistance) newValue = m_maxDistance;
if (newValue < *oR) {
next->push_back((x << 16) + y);
@ -72,7 +72,7 @@ int8_t DistancePath::process(Image* input, Image* output) {
uint16_t* outPixels = output->getPixels16();
for (uint32_t coordinate : m_queue1) {
uint16_t x = coordinate >> 16;
uint16_t y = coordinate & 0xFFFFFFFF;
uint16_t y = coordinate & 0xFFFF;
outRAW[(width * y + x)] = 0;
}
uint16_t iteration = 1;
@ -80,7 +80,7 @@ int8_t DistancePath::process(Image* input, Image* output) {
// process pixel from last time
for (uint32_t coordinate : *last) {
uint16_t x = coordinate >> 16;
uint16_t y = coordinate & 0xFFFFFFFF;
uint16_t y = coordinate & 0xFFFF;
float lastValue = outRAW[y * width + x];
if (y > 0) { // process row on top
processRow(inRAW, outRAW, x, y-1, lastValue, width, next, outPixels, iteration);

67
inbrowser/Gruntfile.js Normal file
View File

@ -0,0 +1,67 @@
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
ts: {
default: {
src: ['ts/**/*.ts'],
outDir: 'tmp/',
options: {
module: 'none',
sourceMap: false,
target: 'es5',
rootDir: 'ts/'
}
},
},
concat: {
js: {
src: ['tmp/**/*.js'],
dest: 'tmp/bootstrap.out'
},
dist: {
src: ['html/index.html'],
dest: 'webroot/index.html',
},
options: {
process: true
}
},
watch: {
jsbootstrapts: {
files: ['ts/**/*.ts'],
tasks: ['ts', 'concat']
},
html: {
files: ['html/*'],
tasks: ['concat']
},
lessdefault: {
files: ['less/*.less'],
tasks: ['less:default', 'concat']
},
},
less: {
default: {
options: {
"strictImports": true,
"compress": true
},
files: {
"tmp/default.css": "less/main.less",
}
},
},
});
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks("grunt-ts");
// Default task(s).
grunt.registerTask('default', ['less', 'ts', 'concat', 'watch']);
grunt.registerTask('release', ['less', 'ts', 'concat']);
};

56
inbrowser/html/index.html Normal file
View File

@ -0,0 +1,56 @@
<html>
<head>
<title>Path Animator Tool</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
<%= grunt.file.read('tmp/default.css') %>
</style>
</head>
<body>
<h2>1. Source image</h2>
<div id="drop-area">
<form>
<p>Select image with the file dialog or by dragging and dropping an image onto the dashed region.</p>
<input type="file" id="fileElem" accept="image/*" />
<label class="button" for="fileElem">Select an image</label>
</form>
<div id="gallery"></div>
</div>
<h2>2. Configure algorithm</h2>
<div id="algorithm">
<form>
<div><label for="algorithm">Select the algorithm to use:</label>
<select name="algorithm" id="alsel">
<option value="distancepath" selected="selected">Distance Path</option>
<option value="noise">Noise</option>
</select>
</div>
<div id="distancepath">
<h3>Distance path</h3>
<div><label for="weightpath">Weight of path</label> <input type="text" name="weightpath" value="1"/></div>
<div><label for="weightblock">Weight of block</label> <input type="text" name="weightblock" value="1000"/></div>
<div><label for="threshold">Treshold for traversable pixel</label> <input type="text" name="treshold" value="210"/></div>
<div><label for="maxdistance">Maximum distance value</label> <input type="text" name="maxdistance" value="3000000"/></div>
<div><label for="startpoints">List of starting points Format:<br />x1,y1 x2,y2 ...</label> <input type="text" name="startpoints" value="0,0"/></div>
</div>
<div id="noise" class="hidden">
<h3>Noise</h3>
Nothing to configure.
</div>
<p>
<button id="run" class="button hidden">run</button>
</p>
</form>
</div>
<h2>3. Output image</h2>
<div id="output">
</div>
<p><button id="save" class="button hidden">Save image</button></p>
<canvas id="outputcanvas" class="hidden"></canvas>
<canvas id="inputcanvas" class="hidden"></canvas>
<script type="text/javascript">
<%= grunt.file.read('tmp/bootstrap.out') %>
</script>
</body>
</html>

66
inbrowser/less/main.less Normal file
View File

@ -0,0 +1,66 @@
body {
background:#000;
color:f9f9f9;
padding:10px;
}
#algorithm {
label {
width:250px;
display:inline-block;
}
}
.hidden {
display:none !important;
}
.running:before {
display:inline-block;
content: ' ';
border-radius: 50%;
border: .5rem solid rgba(0,0,0,0.25);
border-top-color: #fff;
animation: spin 1s infinite linear;
width:16px;
height:16px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#drop-area {
border: 2px dashed #ccc;
border-radius: 20px;
max-width:480px;
width: 50%;
font-family: sans-serif;
margin: 10px 0;
padding: 20px;
&.highlight {
border-color: purple;
}
}
.button {
display: inline-block;
padding: 10px;
color:#000;
background: #ccc;
cursor: pointer;
border-radius: 5px;
border: 1px solid #ccc;
&:hover {
background: #ddd;
}
}
#fileElem {
display: none;
}

5974
inbrowser/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
inbrowser/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "pathanimation",
"version": "1.0.0",
"description": "Javascript version of the Path Animation tool",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "GrumpyDeveloper https://contentnation.net/grumpydevelop",
"license": "MIT",
"dependencies": {
},
"devDependencies": {
"grunt": "^1.4.1",
"grunt-contrib-less": "^3.0.0",
"grunt-contrib-watch": "^1.1.0",
"grunt-ts": "^6.0.0-beta.22",
"grunt-contrib-concat": "^2.0.0"
}
}

View File

@ -0,0 +1,262 @@
/// <reference path="PAImage.ts" />
/// <reference path="PathAnimator.ts" />
/**
* Functionality to handle browser io
*/
class BrowserHandler {
/**
* Drop area for file uploads
*/
private dropArea: HTMLElement;
/**
* Main instace of application
*/
private instance: PathAnimator;
/**
* Indicator flag if the drop area is highlighted
*/
private isHighlighted: boolean;
/**
* Algrothm selection dropdown element
*/
private algorithmElem: HTMLSelectElement;
/**
* Constructor
*/
constructor(instance: PathAnimator) {
this.instance = instance;
this.isHighlighted = false;
this.dropArea = document.getElementById('drop-area');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
this.dropArea.addEventListener(eventName, this.preventDefaults, false);
});
this.dropArea.addEventListener('dragover', this.dragOver.bind(this), false)
this.dropArea.addEventListener('drop', this.dragDrop.bind(this), false)
this.algorithmElem = <HTMLSelectElement>document.getElementById("alsel");
this.algorithmElem.addEventListener("change", this.algorithmChanged.bind(this));
this.algorithmChanged();
document.getElementById("fileElem").addEventListener("change", this.handleSourceFile.bind(this));
document.getElementById("run").addEventListener("click", this.run.bind(this));
document.getElementById("save").addEventListener("click", this.save.bind(this));
}
/**
* Prevent default action for given event
* @param e incoming event
*/
preventDefaults (e: Event) {
e.preventDefault()
e.stopPropagation()
}
/**
* Event when something is dragged over out drag/drop area
*/
dragOver() {
if (!this.isHighlighted) {
this.dropArea.classList.add("highlight");
this.isHighlighted = true;
}
}
/**
* Something was droppen on our drag/drop area
* @param e Incoming event
*/
dragDrop(e: DragEvent) {
let dt = e.dataTransfer
let files = dt.files
this.setSourceFile(files);
if (this.isHighlighted) {
this.dropArea.classList.remove("highlight");
this.isHighlighted = false;
}
}
/**
* A file was selected via upload file button
* @param e: Incoming event
*/
handleSourceFile(e: Event) {
var fs = <HTMLInputElement>e.target;
this.setSourceFile(fs.files);
}
/**
* Set the source file and trigger processing with given file list. Only the first file will be used
* @param fileList the file list generated by the browser on drop or file selection
*/
setSourceFile(fileList: FileList) {
// we only support one image
if (fileList.length > 0) {
var file = fileList.item(0);
this.processFile(file);
}
}
/**
* Process (load) given file, calls fileLoaded when read
* @param file file to process
*/
processFile(file: File) {
let reader = new FileReader()
reader.readAsDataURL(file)
reader.addEventListener("loadend", this.fileLoaded.bind(this));
}
/**
* The file loading from processFile has been finished.
* fileParsed will be called after image was rendered
* @param e progress event (normally file loaded)
*/
fileLoaded(e:ProgressEvent) {
if (e.type === "loadend") { // sanity check
var reader = <FileReader>e.target;
let img = <HTMLImageElement>document.createElement('img');
img.src = <string>reader.result;
img.addEventListener("load", this.fileParsed.bind(this));
}
}
/**
* The image data from file is ready.
* The canvas is filled with the image and the pixel data is extracted
* and set for the main processing instance.
* @param e incoming event
*/
fileParsed(e: Event) {
var img = <HTMLImageElement>e.target;
const canvas = <HTMLCanvasElement>document.getElementById("inputcanvas");
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const rgba = ctx.getImageData(0, 0, img.width, img.height).data;
var image = new PAImage(img.width, img.height, 4);
image.setPixels(rgba);
this.instance.setInputImage(image);
document.getElementById("run").classList.remove("hidden");
}
/**
* The algrothm select dropdown was changed.
* Show and hide algrothm specific inputs
*/
algorithmChanged() {
var div = document.getElementById("algorithm").getElementsByTagName("div");
for (var i = 0; i < div.length; ++i) {
if (div[i].hasAttribute("id")) {
if (div[i].getAttribute("id") === this.algorithmElem.value) {
div[i].classList.remove("hidden");
} else {
div[i].classList.add("hidden");
}
}
}
}
/**
* The run button was pressed.
* The algorithm specific setting are collected and an async timeout call is triggered to run the PathAnimator run function
* @param e incoming Event
*/
run(e: Event) {
e.preventDefault();
e.stopPropagation();
// collect algorithm param
var config = {
algorithm: this.algorithmElem.value
}
var div = document.getElementById(this.algorithmElem.value);
var fields = div.getElementsByTagName("input");
for (var i = 0; i < fields.length; ++i) {
config[fields[i].getAttribute("name")] = fields[i].value;
}
document.getElementById("output").classList.add("running");
document.getElementById("run").classList.add("hidden");
document.getElementById("save").classList.add("hidden");
window.setTimeout(this.instance.run.bind(this.instance, config), 10);
}
/**
* Clear logging output
*/
clearOutput() {
document.getElementById("output").innerHTML = "";
}
/**
* Append message to logging output
* @param data string to display
*/
appendOutput(data: string) {
var p = document.createElement("p");
p.innerText = data;
document.getElementById("output").append(p);
}
/**
* Replace old logging output by new data
* @param data new string to display
*/
replaceOutput(data: string) {
this.clearOutput();
this.appendOutput(data);
}
/**
* Processing has completed
* Show buttons and hide running animation
*/
completed() {
document.getElementById("output").classList.remove("running");
document.getElementById("run").classList.remove("hidden");
document.getElementById("save").classList.remove("hidden");
}
/**
* Show output of the algorithm
* The Canvas is filled with the output of the algorithm.
* Preprocessing is needed, the algorithm exports RGB data, the canvas wants RGBA.
* @param image image to show
*/
showOutput(image: PAImage) {
var output = <HTMLCanvasElement>document.getElementById("outputcanvas");
output.classList.remove("hidden");
var width = image.getWidth();
var height = image.getHeight();
output.width = width;
output.height = height;
var end = width* height * 3;
const ctx = output.getContext('2d');
var outCanvas = ctx.getImageData(0,0, width, height);
const outCanvasPixel = outCanvas.data;
var imagePixel = image.getPixels8();
var offsetCanvas = 0;
var offsetImage = 0;
for (var i = 0; i < end; ++i) {
outCanvasPixel[offsetCanvas++] = imagePixel[offsetImage++];
outCanvasPixel[offsetCanvas++] = imagePixel[offsetImage++];
outCanvasPixel[offsetCanvas++] = imagePixel[offsetImage++];
outCanvasPixel[offsetCanvas++] = 255;
}
ctx.putImageData(outCanvas, 0, 0);
}
/**
* User has clicked the save button
* Trigger download
* @param e incoming Event
*/
save(e: Event) {
var output = <HTMLCanvasElement>document.getElementById("outputcanvas");
var dataURL = output.toDataURL("image/png");
var a = document.createElement('a');
a.href = dataURL;
a.download = "output.png";
document.body.appendChild(a);
a.click();
}
}

80
inbrowser/ts/PAImage.ts Normal file
View File

@ -0,0 +1,80 @@
/**
* Wrapper for pixel data of an image
*/
class PAImage {
/**
* Constructor
* @param width width in pixel
* @param height height in pixel
* @param channels number of channels
*/
constructor(width:number, height:number, channels: number) {
this.width = width;
this.height = height;
this.channels = channels;
}
/**
* Allocate memory for image data
*/
allocateMemory() {
this.pixels = new Uint8ClampedArray(this.width * this. height * this.channels);
}
/**
* Set pixel data
* @param pixels pixel data
*/
setPixels(pixels : Uint8ClampedArray) {
this.pixels = pixels;
}
/**
* Get pixel data
* @return pixel data
*/
getPixels8() : Uint8ClampedArray {
return this.pixels;
}
/**
* Get width of image
* @return width of image
*/
getWidth() : number {
return this.width;
}
/**
* Get height of image
* @return height of image
*/
getHeight() : number {
return this.height;
}
/**
* Get number of channels
* @return number of channels
*/
getNumChannels() : number {
return this.channels;
}
/**
* Image width
*/
private width: number;
/**
* Image height
*/
private height: number;
/**
* Number of channels
*/
private channels: number;
/**
* Image data as RGBA 8 bit per channel
*/
private pixels: Uint8ClampedArray;
};

View File

@ -0,0 +1,81 @@
/// <reference path="BrowserHandler.ts" />
/// <reference path="window.ts" />
/// <reference path="PAImage.ts" />
/// <reference path="algorithm/DistancePath.ts" />
/// <reference path="algorithm/Noise.ts" />
/**
* Main application class
*/
class PathAnimator {
/**
* Browser abstraction instance
*/
private inputHandler:BrowserHandler;
/**
* Input image
*/
private input: PAImage;
/**
* Output image
*/
private output: PAImage;
/**
* Constructor
*/
constructor() {
this.inputHandler = new BrowserHandler(this);
this.input = null;
this.output = null;
}
/**
* Set input image
* @param image image to set as input
*/
setInputImage(image: PAImage) {
this.input = image;
}
/**
* Actual run function that triggerse the processing.
* @param config the algorithm specific config options
*/
run(config: Object) {
var algorithmName = config['algorithm'];
var algorithm: Algorithm = null;
switch(algorithmName) {
case 'distancepath':
algorithm = new DistancePath(config);
break;
case 'noise':
algorithm = new Noise();
break;
}
if (algorithm) {
this.inputHandler.clearOutput();
this.output = new PAImage(this.input.getWidth(), this.input.getHeight(), 3);
this.output.allocateMemory();
algorithm.setCallback(this);
algorithm.setInput(this.input);
algorithm.setOutput(this.output);
algorithm.run();
this.inputHandler.completed();
this.inputHandler.showOutput(this.output);
}
}
/**
* We got a message from out algorithm, display it
* @param message Message to show
* @param append True if message should be appended, false if previous should be cleared
*/
messageCallback(message: string, append: boolean) {
if (append) {
this.inputHandler.appendOutput(message);
} else {
this.inputHandler.replaceOutput(message);
}
}
}

View File

@ -0,0 +1,58 @@
/// <reference path="../PathAnimator.ts" />
/// <reference path="../PAImage.ts" />
/**
* Base class for the algorithms
*/
class Algorithm {
/**
* Main instance
*/
protected callback: PathAnimator;
/**
* Input image
*/
protected input: PAImage;
/**
* Output image
*/
protected output: PAImage;
/**
* Constructor
*/
constructor() {
this.callback = null;
this.output = null;
}
/**
* Set the callback instance
* @param callback The main PathAnimator instance
*/
setCallback(callback: PathAnimator) {
this.callback = callback;
}
/**
* Set the input image
* @param image Input image
*/
setInput(image: PAImage) {
this.input = image;
}
/**
* Set the output image
* @param image Output image
*/
setOutput(image: PAImage) {
this.output = image;
}
/**
* Dummy run function, must be overloaded
*/
run() {
}
}

View File

@ -0,0 +1,158 @@
/// <reference path="Algorithm.ts" />
/**
* Distance Path algorithm implementation
*/
class DistancePath extends Algorithm {
/**
* Pixels updated by last iteration. Value = x<<16 + y
*/
private last: number[];
/**
* Pixels updated by current iteration. Value = x<<16 + y
*/
private next: number[];
/**
* Weight of a block (Pixel with value 0)
*/
private weightBlock: number;
/**
* Weight of a path (Pixel with value 255)
*/
private weightPath: number;
/**
* Treshold value when a pixel becomes a path (0...255)
*/
private treshold: number;
/**
* Maximum distance before path becomes no longer traversable
*/
private maxDistance: number;
/**
* Input pixel data (RGBA)
*/
private inRAW: Uint8ClampedArray;
/**
* Temporary distance map
*/
private outRAW: number[];
/**
* Constructor
* @param config Parameters from UI
*/
constructor(config: Object) {
super();
this.last = [];
this.next = [];
this.weightBlock = parseFloat(config['weightblock']);
this.weightPath = parseFloat(config['weightpath']);
this.treshold = parseInt(config['treshold'], 10);
this.maxDistance = parseFloat(config['maxdistance']);
// start pixel
var start = config['startpoints'].split(" ");
for (var i = 0; i < start.length; ++i) {
var pos = start[i].split(",");
this.last.push((parseInt(pos[0]) << 16) + parseInt(pos[1]));
}
}
/**
* Process a single pixel.
* @param x X-coordinate
* @param y Y-coordinate
* @param lastValue Last value of the pixel that triggered (re)calculation
* @param width image width
*/
processPixel(x: number, y: number, lastValue: number, width: number) {
var offset = (y * width + x);
var inOffset = offset * 4;
var avg = (this.inRAW[inOffset] + this.inRAW[inOffset + 1] + this.inRAW[inOffset + 2]) / 765;
var newValue = lastValue + (1 - avg) * (this.weightBlock - this.weightPath) + this.weightPath;
if (newValue > this.maxDistance) newValue = this.maxDistance;
if (newValue < this.outRAW[offset]) {
this.next.push((x << 16) + y);
this.outRAW[offset] = newValue;
}
}
/**
* Run the algorithm
*/
run() {
var width = this.input.getWidth();
var height = this.input.getHeight();
this.inRAW = this.input.getPixels8();
var end = width * height;
// allocate output storage and set to m_maxDistance
this.outRAW = new Array<number>(width * height);
for (var i = 0; i < end; i++) this.outRAW[i] = this.maxDistance;
// set start pixels to distance of 0
this.last.forEach((coordinate) => {
var x = coordinate >> 16;
var y = coordinate & 0xFFFF;
this.outRAW[(width * y + x)] = 0;
});
var outPixels = this.output.getPixels8();
var iteration = 0;
do {
// process pixel from last time
for (var i = 0; i < this.last.length; ++i) {
var coordinate = this.last[i];
var x = coordinate >> 16;
var y = coordinate & 0xFFFF;
var lastValue = this.outRAW[y * width + x];
if (y > 0) { // process row on top
if (x > 0) this.processPixel(x - 1, y - 1, lastValue, width);
this.processPixel(x, y - 1, lastValue, width);
if (x < (width - 1)) this.processPixel(x + 1, y - 1, lastValue, width);
}
if (x > 0) this.processPixel(x - 1, y, lastValue, width);
if (x < (width - 1)) this.processPixel(x + 1, y, lastValue, width);
if (y < (height - 1)) { // process row on bottom
if (x > 0) this.processPixel(x - 1, y + 1, lastValue, width);
this.processPixel(x, y + 1, lastValue, width);
if (x < (width - 1)) this.processPixel(x + 1, y + 1, lastValue, width);
}
}
++iteration;
// clear out old list
this.last = this.next;
this.next = [];
if (iteration % 1000 == 0) {
this.callback.messageCallback("iteration: " + iteration, false);
}
if (iteration > 10000) break;
} while (this.last.length > 0);
// find max value
var max = 0;
var threshold = this.treshold * 3; // multiply by 3 here saves many division from sum to average in the loops below
// could be optimized, but it is only run once for every pixel
for (var i = 0; i < end; ++i) {
var offset = i * 4;
var sum = (this.inRAW[offset] + this.inRAW[offset + 1] + this.inRAW[offset + 2]);
// only update max if it is larger than previous, but still smaller than our max distance and larger as the threshhold (visible path cheat)
if (this.outRAW[i] > max && this.outRAW[i] < this.maxDistance && (sum > threshold)) max = this.outRAW[i];
}
this.callback.messageCallback("took " + iteration + " iterations max value: " + max, false);
max = 65535.0 / max; // results in 0 ... 65535 range
// write result to output
var inOffset = 0;
var outOffset = 0;
for (var i = 0; i < end; ++i) {
var sum = (this.inRAW[inOffset++] + this.inRAW[inOffset++] + this.inRAW[inOffset++]);
++inOffset;
if (sum > threshold) {
var value = this.outRAW[i] * max;
outPixels[outOffset++] = value >> 8; // red = distance high byte
outPixels[outOffset++] = value & 0xFF; // green = distance low byte
outPixels[outOffset++] = 255; // blue= a pathable pixel
} else {
outPixels[outOffset++] = 0;
outPixels[outOffset++] = 0;
outPixels[outOffset++] = 0;
}
}
}
}

View File

@ -0,0 +1,19 @@
/// <reference path="Algorithm.ts" />
/**
* Demo algorithm that fills image with noise
*/
class Noise extends Algorithm {
/**
* Run the algorithm
*/
run() {
var width = this.output.getWidth();
var height = this.output.getHeight();
var pixels = this.output.getPixels8();
var end = width * height * 3;
for (var i = 0; i < end; ++i) {
pixels[i] = Math.random()*255;
}
}
}

11
inbrowser/ts/init.ts Normal file
View File

@ -0,0 +1,11 @@
/// <reference path="PathAnimator.ts" />
/**
* Main entry function
*/
/// create our class as soon as the document is loaded
document.addEventListener("readystatechange", function(event: Event) {
if (!window.pa) {
window.pa = new PathAnimator();
}
});

11
inbrowser/ts/window.ts Normal file
View File

@ -0,0 +1,11 @@
/// <reference path="PathAnimator.ts" />
/**
* extend Window to make our global instance available (for debugging purposes)
*/
interface Window {
/**
* our instance
*/
pa: PathAnimator;
}

View File

@ -0,0 +1,462 @@
<html>
<head>
<title>Path Animator Tool</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body{background:#000;color:f9f9f9;padding:10px}#algorithm label{width:250px;display:inline-block}.hidden{display:none !important}.running:before{display:inline-block;content:' ';border-radius:50%;border:.5rem solid rgba(0,0,0,0.25);border-top-color:#fff;animation:spin 1s infinite linear;width:16px;height:16px}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}#drop-area{border:2px dashed #ccc;border-radius:20px;max-width:480px;width:50%;font-family:sans-serif;margin:10px 0;padding:20px}#drop-area.highlight{border-color:purple}.button{display:inline-block;padding:10px;color:#000;background:#ccc;cursor:pointer;border-radius:5px;border:1px solid #ccc}.button:hover{background:#ddd}#fileElem{display:none}
</style>
</head>
<body>
<h2>1. Source image</h2>
<div id="drop-area">
<form>
<p>Select image with the file dialog or by dragging and dropping an image onto the dashed region.</p>
<input type="file" id="fileElem" accept="image/*" />
<label class="button" for="fileElem">Select an image</label>
</form>
<div id="gallery"></div>
</div>
<h2>2. Configure algorithm</h2>
<div id="algorithm">
<form>
<div><label for="algorithm">Select the algorithm to use:</label>
<select name="algorithm" id="alsel">
<option value="distancepath" selected="selected">Distance Path</option>
<option value="noise">Noise</option>
</select>
</div>
<div id="distancepath">
<h3>Distance path</h3>
<div><label for="weightpath">Weight of path</label> <input type="text" name="weightpath" value="1"/></div>
<div><label for="weightblock">Weight of block</label> <input type="text" name="weightblock" value="1000"/></div>
<div><label for="threshold">Treshold for traversable pixel</label> <input type="text" name="treshold" value="210"/></div>
<div><label for="maxdistance">Maximum distance value</label> <input type="text" name="maxdistance" value="3000000"/></div>
<div><label for="startpoints">List of starting points Format:<br />x1,y1 x2,y2 ...</label> <input type="text" name="startpoints" value="0,0"/></div>
</div>
<div id="noise" class="hidden">
<h3>Noise</h3>
Nothing to configure.
</div>
<p>
<button id="run" class="button hidden">run</button>
</p>
</form>
</div>
<h2>3. Output image</h2>
<div id="output">
</div>
<p><button id="save" class="button hidden">Save image</button></p>
<canvas id="outputcanvas" class="hidden"></canvas>
<canvas id="inputcanvas" class="hidden"></canvas>
<script type="text/javascript">
var Algorithm = (function () {
function Algorithm() {
this.callback = null;
this.output = null;
}
Algorithm.prototype.setCallback = function (callback) {
this.callback = callback;
};
Algorithm.prototype.setInput = function (image) {
this.input = image;
};
Algorithm.prototype.setOutput = function (image) {
this.output = image;
};
Algorithm.prototype.run = function () {
};
return Algorithm;
}());
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var DistancePath = (function (_super) {
__extends(DistancePath, _super);
function DistancePath(config) {
var _this = _super.call(this) || this;
_this.last = [];
_this.next = [];
_this.weightBlock = parseFloat(config['weightblock']);
_this.weightPath = parseFloat(config['weightpath']);
_this.treshold = parseInt(config['treshold'], 10);
_this.maxDistance = parseFloat(config['maxdistance']);
var start = config['startpoints'].split(" ");
for (var i = 0; i < start.length; ++i) {
var pos = start[i].split(",");
_this.last.push((parseInt(pos[0]) << 16) + parseInt(pos[1]));
}
return _this;
}
DistancePath.prototype.processPixel = function (x, y, lastValue, width) {
var offset = (y * width + x);
var inOffset = offset * 4;
var avg = (this.inRAW[inOffset] + this.inRAW[inOffset + 1] + this.inRAW[inOffset + 2]) / 765;
var newValue = lastValue + (1 - avg) * (this.weightBlock - this.weightPath) + this.weightPath;
if (newValue > this.maxDistance)
newValue = this.maxDistance;
if (newValue < this.outRAW[offset]) {
this.next.push((x << 16) + y);
this.outRAW[offset] = newValue;
}
};
DistancePath.prototype.run = function () {
var _this = this;
var width = this.input.getWidth();
var height = this.input.getHeight();
this.inRAW = this.input.getPixels8();
var end = width * height;
this.outRAW = new Array(width * height);
for (var i = 0; i < end; i++)
this.outRAW[i] = this.maxDistance;
this.last.forEach(function (coordinate) {
var x = coordinate >> 16;
var y = coordinate & 0xFFFF;
_this.outRAW[(width * y + x)] = 0;
});
var outPixels = this.output.getPixels8();
var iteration = 0;
do {
for (var i = 0; i < this.last.length; ++i) {
var coordinate = this.last[i];
var x = coordinate >> 16;
var y = coordinate & 0xFFFF;
var lastValue = this.outRAW[y * width + x];
if (y > 0) {
if (x > 0)
this.processPixel(x - 1, y - 1, lastValue, width);
this.processPixel(x, y - 1, lastValue, width);
if (x < (width - 1))
this.processPixel(x + 1, y - 1, lastValue, width);
}
if (x > 0)
this.processPixel(x - 1, y, lastValue, width);
if (x < (width - 1))
this.processPixel(x + 1, y, lastValue, width);
if (y < (height - 1)) {
if (x > 0)
this.processPixel(x - 1, y + 1, lastValue, width);
this.processPixel(x, y + 1, lastValue, width);
if (x < (width - 1))
this.processPixel(x + 1, y + 1, lastValue, width);
}
}
++iteration;
this.last = this.next;
this.next = [];
if (iteration % 1000 == 0) {
this.callback.messageCallback("iteration: " + iteration, false);
}
if (iteration > 10000)
break;
} while (this.last.length > 0);
var max = 0;
var threshold = this.treshold * 3;
for (var i = 0; i < end; ++i) {
var offset = i * 4;
var sum = (this.inRAW[offset] + this.inRAW[offset + 1] + this.inRAW[offset + 2]);
if (this.outRAW[i] > max && this.outRAW[i] < this.maxDistance && (sum > threshold))
max = this.outRAW[i];
}
this.callback.messageCallback("took " + iteration + " iterations max value: " + max, false);
max = 65535.0 / max;
var inOffset = 0;
var outOffset = 0;
for (var i = 0; i < end; ++i) {
var sum = (this.inRAW[inOffset++] + this.inRAW[inOffset++] + this.inRAW[inOffset++]);
++inOffset;
if (sum > threshold) {
var value = this.outRAW[i] * max;
outPixels[outOffset++] = value >> 8;
outPixels[outOffset++] = value & 0xFF;
outPixels[outOffset++] = 255;
}
else {
outPixels[outOffset++] = 0;
outPixels[outOffset++] = 0;
outPixels[outOffset++] = 0;
}
}
};
return DistancePath;
}(Algorithm));
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var Noise = (function (_super) {
__extends(Noise, _super);
function Noise() {
return _super !== null && _super.apply(this, arguments) || this;
}
Noise.prototype.run = function () {
var width = this.output.getWidth();
var height = this.output.getHeight();
var pixels = this.output.getPixels8();
var end = width * height * 3;
for (var i = 0; i < end; ++i) {
pixels[i] = Math.random() * 255;
}
};
return Noise;
}(Algorithm));
var BrowserHandler = (function () {
function BrowserHandler(instance) {
var _this = this;
this.instance = instance;
this.isHighlighted = false;
this.dropArea = document.getElementById('drop-area');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function (eventName) {
_this.dropArea.addEventListener(eventName, _this.preventDefaults, false);
});
this.dropArea.addEventListener('dragover', this.dragOver.bind(this), false);
this.dropArea.addEventListener('drop', this.dragDrop.bind(this), false);
this.algorithmElem = document.getElementById("alsel");
this.algorithmElem.addEventListener("change", this.algorithmChanged.bind(this));
this.algorithmChanged();
document.getElementById("fileElem").addEventListener("change", this.handleSourceFile.bind(this));
document.getElementById("run").addEventListener("click", this.run.bind(this));
document.getElementById("save").addEventListener("click", this.save.bind(this));
}
BrowserHandler.prototype.preventDefaults = function (e) {
e.preventDefault();
e.stopPropagation();
};
BrowserHandler.prototype.dragOver = function () {
if (!this.isHighlighted) {
this.dropArea.classList.add("highlight");
this.isHighlighted = true;
}
};
BrowserHandler.prototype.dragDrop = function (e) {
var dt = e.dataTransfer;
var files = dt.files;
this.setSourceFile(files);
if (this.isHighlighted) {
this.dropArea.classList.remove("highlight");
this.isHighlighted = false;
}
};
BrowserHandler.prototype.handleSourceFile = function (e) {
var fs = e.target;
this.setSourceFile(fs.files);
};
BrowserHandler.prototype.setSourceFile = function (fileList) {
if (fileList.length > 0) {
var file = fileList.item(0);
this.processFile(file);
}
};
BrowserHandler.prototype.processFile = function (file) {
var reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener("loadend", this.fileLoaded.bind(this));
};
BrowserHandler.prototype.fileLoaded = function (e) {
if (e.type === "loadend") {
var reader = e.target;
var img = document.createElement('img');
img.src = reader.result;
img.addEventListener("load", this.fileParsed.bind(this));
}
};
BrowserHandler.prototype.fileParsed = function (e) {
var img = e.target;
var canvas = document.getElementById("inputcanvas");
var ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
var rgba = ctx.getImageData(0, 0, img.width, img.height).data;
var image = new PAImage(img.width, img.height, 4);
image.setPixels(rgba);
this.instance.setInputImage(image);
document.getElementById("run").classList.remove("hidden");
};
BrowserHandler.prototype.algorithmChanged = function () {
var div = document.getElementById("algorithm").getElementsByTagName("div");
for (var i = 0; i < div.length; ++i) {
if (div[i].hasAttribute("id")) {
if (div[i].getAttribute("id") === this.algorithmElem.value) {
div[i].classList.remove("hidden");
}
else {
div[i].classList.add("hidden");
}
}
}
};
BrowserHandler.prototype.run = function (e) {
e.preventDefault();
e.stopPropagation();
var config = {
algorithm: this.algorithmElem.value
};
var div = document.getElementById(this.algorithmElem.value);
var fields = div.getElementsByTagName("input");
for (var i = 0; i < fields.length; ++i) {
config[fields[i].getAttribute("name")] = fields[i].value;
}
document.getElementById("output").classList.add("running");
document.getElementById("run").classList.add("hidden");
document.getElementById("save").classList.add("hidden");
window.setTimeout(this.instance.run.bind(this.instance, config), 10);
};
BrowserHandler.prototype.clearOutput = function () {
document.getElementById("output").innerHTML = "";
};
BrowserHandler.prototype.appendOutput = function (data) {
var p = document.createElement("p");
p.innerText = data;
document.getElementById("output").append(p);
};
BrowserHandler.prototype.replaceOutput = function (data) {
this.clearOutput();
this.appendOutput(data);
};
BrowserHandler.prototype.completed = function () {
document.getElementById("output").classList.remove("running");
document.getElementById("run").classList.remove("hidden");
document.getElementById("save").classList.remove("hidden");
};
BrowserHandler.prototype.showOutput = function (image) {
var output = document.getElementById("outputcanvas");
output.classList.remove("hidden");
var width = image.getWidth();
var height = image.getHeight();
output.width = width;
output.height = height;
var end = width * height * 3;
var ctx = output.getContext('2d');
var outCanvas = ctx.getImageData(0, 0, width, height);
var outCanvasPixel = outCanvas.data;
var imagePixel = image.getPixels8();
var offsetCanvas = 0;
var offsetImage = 0;
for (var i = 0; i < end; ++i) {
outCanvasPixel[offsetCanvas++] = imagePixel[offsetImage++];
outCanvasPixel[offsetCanvas++] = imagePixel[offsetImage++];
outCanvasPixel[offsetCanvas++] = imagePixel[offsetImage++];
outCanvasPixel[offsetCanvas++] = 255;
}
ctx.putImageData(outCanvas, 0, 0);
};
BrowserHandler.prototype.save = function (e) {
var output = document.getElementById("outputcanvas");
var dataURL = output.toDataURL("image/png");
var a = document.createElement('a');
a.href = dataURL;
a.download = "output.png";
document.body.appendChild(a);
a.click();
};
return BrowserHandler;
}());
document.addEventListener("readystatechange", function (event) {
if (!window.pa) {
window.pa = new PathAnimator();
}
});
var PAImage = (function () {
function PAImage(width, height, channels) {
this.width = width;
this.height = height;
this.channels = channels;
}
PAImage.prototype.allocateMemory = function () {
this.pixels = new Uint8ClampedArray(this.width * this.height * this.channels);
};
PAImage.prototype.setPixels = function (pixels) {
this.pixels = pixels;
};
PAImage.prototype.getPixels8 = function () {
return this.pixels;
};
PAImage.prototype.getWidth = function () {
return this.width;
};
PAImage.prototype.getHeight = function () {
return this.height;
};
PAImage.prototype.getNumChannels = function () {
return this.channels;
};
return PAImage;
}());
;
var PathAnimator = (function () {
function PathAnimator() {
this.inputHandler = new BrowserHandler(this);
this.input = null;
this.output = null;
}
PathAnimator.prototype.setInputImage = function (image) {
this.input = image;
};
PathAnimator.prototype.run = function (config) {
var algorithmName = config['algorithm'];
var algorithm = null;
switch (algorithmName) {
case 'distancepath':
algorithm = new DistancePath(config);
break;
case 'noise':
algorithm = new Noise();
break;
}
if (algorithm) {
this.inputHandler.clearOutput();
this.output = new PAImage(this.input.getWidth(), this.input.getHeight(), 3);
this.output.allocateMemory();
algorithm.setCallback(this);
algorithm.setInput(this.input);
algorithm.setOutput(this.output);
algorithm.run();
this.inputHandler.completed();
this.inputHandler.showOutput(this.output);
}
};
PathAnimator.prototype.messageCallback = function (message, append) {
if (append) {
this.inputHandler.appendOutput(message);
}
else {
this.inputHandler.replaceOutput(message);
}
};
return PathAnimator;
}());
</script>
</body>
</html>