Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes
File renamed without changes.
18 changes: 14 additions & 4 deletions demo/index.html → docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
<link rel="stylesheet" type="text/css" href="default.css">
<script type="text/javascript" src="../index.js"></script>
<script type="text/javascript">
const bucket = new filler({
const bucket = new waveFiller({
canvasId: 'canvas',
imageSrc: 'maze.png',
threshold: 60, // maximum deviance in color channell value allowed for a pixel to be considered blank
blank: [255, 255, 255, 255], // white - set it to whatever color is considered blank in the image
pixel: [255, 0, 0, 50], // red - set it to whatever fill color you want as RGBA
radius: 50, // wave size in pixels rendered per frame
radius: 10, // wave size in pixels rendered per frame
fps: 60, // frame limiter; the rendered frames per second will be limited to approximately this value; actual fps can be lower depending on your CPU
fit: {
width: 800, // this will be the actual canvas width; height will be calculated relative to this width
Expand All @@ -20,10 +20,20 @@
workerCount: 6, // number of web workers to be used
minWorkerLoad: 500, // minimum number of shore pixels, if more are available, to be assigned to a web worker
computeAhead: true, // set to true to compute upcoming frames before current frame is done for faster overall rendering; warning: wave is no longer an advancing circle when filling large areas
libraryPath: '../' // path to library directory relative to current context
libraryPath: '../', // path to library directory relative to current context
silent: false // set to true to disable console logs
});
window.addEventListener("load", () => {
bucket.initialize();
bucket.initialize()
.then((result) => {
console.log(`yep, ${result} workers are ready :)`);
bucket.canvas.onclick = (event) => {
bucket.click(event.clientX, event.clientY);
}
})
.catch((error) => {
console.log('oops; error...');
});
});
</script>
</head>
Expand Down
File renamed without changes
186 changes: 100 additions & 86 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function filler(options) {
function waveFiller(options) {
this.threshold = options.threshold || 20; // maximum deviance in color channell value allowed for a pixel to be considered blank
this.blank = options.blank || [255, 255, 255, 255]; // white - set it to whatever color is considered blank in the image
this.pixel = options.pixel || [255, 0, 0, 50]; // red - set it to whatever fill color you want as RGBA
Expand All @@ -8,97 +8,95 @@ function filler(options) {
this.minWorkerLoad = options.minWorkerLoad || 500; // minimum number of shore pixels, if more are available, to be assigned to a web worker
this.computeAhead = options.computeAhead; // set to true to compute upcoming frames before current frame is done for faster overall rendering; warning: wave is no longer an advancing circle when filling large areas
this.libraryPath = options.libraryPath || './' // path to library directory relative to current context
this.silent = options.silent // set to true to disable console logs
const frameTime = 1000 / this.fps;
let skipFrame = false;
this.initialize = async () => {
this.canvas = document.getElementById(options.canvasId);
this.context = this.canvas.getContext('2d');
this.paint(options.fit.width, options.fit.height, options.fit.resize);
this.canvas.onclick = (event) => {
this.click(event.clientX, event.clientY);
}
}
this.paint = (width, height, resize) => { // paint image in canvas
this.image = new Image();
this.image.src = options.imageSrc;
this.image.onload = async () => {
this.initialize = () => {
return new Promise (async (resolve, reject) => {
try {
let resized;
if (width) {
height = this.image.height / this.image.width * width;
}
else if (height) {
width = this.image.width / this.image.height * height;
}
else {
resized = this.fit();
width = resized.width;
height = resized.height;
}
this.canvas.width = width;
this.canvas.height = height;
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.context.drawImage(this.image, 0, 0, this.canvas.width, this.canvas.height);
this.pixels = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
if (resize) {
resized = this.fit();
this.canvas.style.width = resized.width;
this.canvas.style.height = resized.height;
}
if (this.canvas.offsetWidth < window.innerWidth)
this.canvas.style.left = (window.innerWidth - this.canvas.offsetWidth) / 2 + 'px';
if (this.canvas.offsetHeight < window.innerHeight)
this.canvas.style.top = (window.innerHeight - this.canvas.offsetHeight) / 2 + 'px';
this.canvasScale = this.canvas.width / this.canvas.offsetWidth;
this.workers = [];
this.worked = 0;
const workerBlob = await (await fetch(`${options.libraryPath}worker.js`)).blob();
for (let index = 0; index < this.workerCount; index++) {
const worker = new Worker(URL.createObjectURL(workerBlob));
worker.working = false;
worker.onmessage = (message) => {
switch (message.data.status) {
case 'initDone':
this.worked++;
if (this.worked == this.workerCount) {
console.log(`web worker init done; ${this.worked} web workers ready`);
}
break;
case 'done':
this.handleWorkerDone(message.data.output);
break;
}
this.canvas = document.getElementById(options.canvasId);
this.context = this.canvas.getContext('2d');
let width = options.fit.width;
let height = options.fit.height;
let resize = options.fit.resize;
this.image = new Image();
this.image.src = options.imageSrc;
this.image.onload = async () => {
let resized;
if (width) {
height = this.image.height / this.image.width * width;
}
else if (height) {
width = this.image.width / this.image.height * height;
}
worker.onerror = (error) => {
console.log(`worker ${index} error`);
console.log(error);
else {
resized = this.fit();
width = resized.width;
height = resized.height;
}
worker.onmessageerror = (error) => {
console.log(`worker ${index} message error`);
console.log(error);
this.canvas.width = width;
this.canvas.height = height;
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.context.drawImage(this.image, 0, 0, this.canvas.width, this.canvas.height);
this.pixels = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
if (resize) {
resized = this.fit();
this.canvas.style.width = resized.width;
this.canvas.style.height = resized.height;
}
worker.postMessage({
type: 'init',
input: {
index,
threshold: this.threshold,
blank: this.blank,
pixel: this.pixel,
radius: this.radius,
width: this.canvas.width,
height: this.canvas.height,
pixels: this.pixels.data,
done: {}
if (this.canvas.offsetWidth < window.innerWidth)
this.canvas.style.left = (window.innerWidth - this.canvas.offsetWidth) / 2 + 'px';
if (this.canvas.offsetHeight < window.innerHeight)
this.canvas.style.top = (window.innerHeight - this.canvas.offsetHeight) / 2 + 'px';
this.canvasScale = this.canvas.width / this.canvas.offsetWidth;
this.workers = [];
let initialized = 0;
const workerBlob = await (await fetch(`${options.libraryPath}worker.js`)).blob();
for (let index = 0; index < this.workerCount; index++) {
const worker = new Worker(URL.createObjectURL(workerBlob));
worker.working = false;
worker.onmessage = (message) => {
switch (message.data.status) {
case 'initDone':
initialized++;
if (initialized == this.workerCount) {
this.log(`web worker init done; ${initialized} web workers ready`);
resolve(initialized);
}
break;
case 'done':
this.handleWorkerDone(message.data.output);
break;
}
}
worker.onerror = (error) => {
this.log([`worker ${index} error`, error]);
}
});
this.workers.push(worker);
worker.onmessageerror = (error) => {
this.log([`worker ${index} message error`, error]);
}
worker.postMessage({
type: 'init',
input: {
index,
threshold: this.threshold,
blank: this.blank,
pixel: this.pixel,
radius: this.radius,
width: this.canvas.width,
height: this.canvas.height,
pixels: this.pixels.data,
done: {}
}
});
this.workers.push(worker);
}
}
}
catch (error) {
console.log('initialize error');
console.log(error);
this.log(['initialize error', error]);
}
}
});
}
this.fit = () => { // resize image to fit canvas
let width;
Expand Down Expand Up @@ -242,7 +240,8 @@ function filler(options) {
currentFrame = this.frames[this.frame];
if (!currentFrame?.shore.length) { // animation done
this.end = window.performance.now();
console.log(`done in ${this.end - this.start} ms`);
this.runTime = this.end - this.start;
this.log(`done in ${this.runTime} ms @ ${((Object.keys(this.frames).length - 1) / this.runTime * 1000).toFixed(2)} fps`);
this.locked = false;
}
else {
Expand All @@ -256,20 +255,35 @@ function filler(options) {
}
window.requestAnimationFrame(this.checkFrameReady);
}
this.click = (x, y) => {
this.fill = (x, y) => {
if (this.locked) {
console.log('> nope; locked.');
this.log('locked; already running');
return;
}
this.locked = true;
this.frame = 0;
this.frames = {};
x = Math.floor((x - this.canvas.offsetLeft) * this.canvasScale);
y = Math.floor((y - this.canvas.offsetTop) * this.canvasScale);
this.createFrame(this.frame);
this.frames[this.frame].shore = [[x, y]];
this.start = window.performance.now();
this.computeNextFrame();
this.assignWork();
}
this.click = (x, y) => { // computes x, y click event coordinates relative to canvas pixels
x = Math.floor((x - this.canvas.offsetLeft) * this.canvasScale);
y = Math.floor((y - this.canvas.offsetTop) * this.canvasScale);
this.fill(x, y);
}
this.log = (input) => {
if (options.silent) {
return;
}
const isArray = Array.isArray(input);
console.log(`waveFiller: ${!isArray ? input : ''}`);
if (isArray) {
for (let i = 0; i < input.length; i++) {
console.log(input[i]);
}
}
}
}