I am attempting to write a Darkroom.JS plugin that will transform white space in images to transparency.
I have used this answer (solely canvas based) to write this code:
(function() {
'use strict';
var Transparency = Darkroom.Transformation.extend({
applyTransformation: function(canvas, image, next) {
console.log(canvas);
console.log(image);
var context = canvas.getContext("2d");
var upperContext = $('.upper-canvas').get(0).getContext("2d");
var imageData = context.getImageData(0, 0, image.getWidth(), image.getHeight());
//var upperImageData = upperContext.createImageData(image.getWidth(), image.getHeight());
console.log("apply transformation called");
for(var i = 0, n = imageData.data.length; i < n; i +=4){
var r = imageData.data[i],
g = imageData.data[i+1],
b = imageData.data[i+2];
if(r >= 230 && g >= 230 && b >= 230){
imageData.data[i] = 0;
imageData.data[i+1] = 0;
imageData.data[i+2] = 0;
imageData.data[i+3] = 1;
}
};
context.putImageData(imageData, 0, 0);
upperContext.putImageData(imageData, 0, 0);
//canvas.renderAll();
next();
}
});
Darkroom.plugins['transparency'] = Darkroom.Plugin.extend({
defaults: {
clearWhiteSpace: function() {
this.darkroom.applyTransformation(
new Transparency()
);
}
},
initialize: function InitializeDarkroomTransparencyPlugin() {
var buttonGroup = this.darkroom.toolbar.createButtonGroup();
this.destroyButton = buttonGroup.createButton({
image: 'wand' //Magic Wand by John O'Shea from the Noun Project
});
this.destroyButton.addEventListener('click', this.options.clearWhiteSpace.bind(this));
},
});
})();
(I should also note I based the structure of the plugin off of the existing rotate plugin)
The code does get called, and I do not currently have it in the code (for performance reasons) but a log statement indicated that the if block where the pixel editing is done also gets called.
To verify, I presently have the pixels set to fully opacity and black (instead of transparent so that I can see the effects of editing).
Also, I noticed that Darkroom.JS seems to generate two canvas objects, an upper canvas and lower canvas. The object passed to the transform function is the "lower canvas" object, so I even tried using jQuery to grab the "upper" one and set the image data on that, to no avail.
What am I missing?
I was focusing my search for an answer far too much on Darkroom.JS.
Darkroom.JS is just a layer on top of Fabric.JS, and this answer holds the key:
fabric js or imagick remove white from image
I actually used the second answer and it works perfectly:
So there is a filter in Fabric.js that does just that.
http://fabricjs.com/docs/fabric.Image.filters.RemoveWhite.html
var filter = new fabric.Image.filters.RemoveWhite({ threshold: 40,
distance: 140 }); image.filters.push(filter);
image.applyFilters(canvas.renderAll.bind(canvas));
Here is my completed code (with some extraneous details removed to simplify):
fabric.Image.fromURL(imgData.URL, function(logoImg){
canvas.add(logoImg);
var threshold = 40;
var whitespace = function(){
var filter = new fabric.Image.filters.RemoveWhite({
threshold: threshold,
distance: 140
});
threshold+=20;
logoImg.filters.push(filter);
logoImg.applyFilters(canvas.renderAll.bind(canvas));
};
});
Related
I'm trying to use Fabric.js with Fabric Brush This issue that I'm running into is that Fabric Brush only puts the brush strokes onto the Top Canvas and not the lower canvas. (The stock brushes in fabric.js save to the bottom canvas) I think I need to convert "this.canvas.contextTop.canvas" to an object and add that object to the the lower canvas. Any ideas?
I've tried running:
this.canvas.add(this.canvas.contextTop)
in
onMouseUp: function (pointer) {this.canvas.add(this.canvas.contextTop)}
But I'm getting the error
Uncaught TypeError: obj._set is not a function
So the contextTop is CanvasHTMLElement context. You cannot add it.
You can add to the fabricJS canvas just fabric.Object derived classes.
Look like is not possible for now.
They draw as pixel effect and then they allow you to export as an image.
Would be nice to extend fabricJS brush interface to create redrawable objects.
As of now with fabricJS and that particular version of fabric brush, the only thing you can do is:
var canvas = new fabric.Canvas(document.getElementById('c'))
canvas.freeDrawingBrush = new fabric.CrayonBrush(canvas, {
width: 70,
opacity: 0.6,
color: "#ff0000"
});
canvas.isDrawingMode = true
canvas.on('mouse:up', function(opt) {
if (canvas.isDrawingMode) {
var c = fabric.util.copyCanvasElement(canvas.upperCanvasEl);
var img = new fabric.Image(c);
canvas.contextTopDirty = true;
canvas.add(img);
canvas.isDrawingMode = false;
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.1/fabric.min.js"></script>
<script src="https://tennisonchan.github.io/fabric-brush/bower_components/fabric-brush/dist/fabric-brush.min.js"></script>
<button>Enter free drawing</button>
<canvas id="c" width="500" height="500" ></canvas>
That is just creating an image from the contextTop and add as an object.
I have taken the approach suggested by AndreaBogazzi and modified the Fabric Brush so that it does the transfer from upper to lower canvas (as an image) internal to Fabric Brush. I also used some code I found which crops the image to a smaller bounding box so that is smaller than the full size of the canvas. Each of the brushes in Fabric Brush has an onMouseUp function where the code should be placed. Using the case of the SprayBrush, the original code here was:
onMouseUp: function(pointer) {
},
And it is replaced with this code:
onMouseUp: function(pointer){
function trimbrushandcopytocanvas() {
let ctx = this.canvas.contextTop;
let pixels = ctx.getImageData(0, 0, canvas.upperCanvasEl.width, canvas.upperCanvasEl.height),
l = pixels.data.length,
bound = {
top: null,
left: null,
right: null,
bottom: null
},
x, y;
// Iterate over every pixel to find the highest
// and where it ends on every axis ()
for (let i = 0; i < l; i += 4) {
if (pixels.data[i + 3] !== 0) {
x = (i / 4) % canvas.upperCanvasEl.width;
y = ~~((i / 4) / canvas.upperCanvasEl.width);
if (bound.top === null) {
bound.top = y;
}
if (bound.left === null) {
bound.left = x;
} else if (x < bound.left) {
bound.left = x;
}
if (bound.right === null) {
bound.right = x;
} else if (bound.right < x) {
bound.right = x;
}
if (bound.bottom === null) {
bound.bottom = y;
} else if (bound.bottom < y) {
bound.bottom = y;
}
}
}
// Calculate the height and width of the content
var trimHeight = bound.bottom - bound.top,
trimWidth = bound.right - bound.left,
trimmed = ctx.getImageData(bound.left, bound.top, trimWidth, trimHeight);
// generate a second canvas
var renderer = document.createElement('canvas');
renderer.width = trimWidth;
renderer.height = trimHeight;
// render our ImageData on this canvas
renderer.getContext('2d').putImageData(trimmed, 0, 0);
var img = new fabric.Image(renderer,{
scaleY: 1./fabric.devicePixelRatio,
scaleX: 1./fabric.devicePixelRatio,
left: bound.left/fabric.devicePixelRatio,
top:bound.top/fabric.devicePixelRatio
});
this.canvas.clearContext(ctx);
canvas.add(img);
}
setTimeout(trimbrushandcopytocanvas, this._interval); // added delay because last spray was on delay and may not have finished
},
The setTimeout function was used because Fabric Brush could still be drawing to the upper canvas after the mouseup event occurred, and there were occasions where the brush would continue painting the upper canvas after its context was cleared.
I'm trying to make a snake game with HTML5 using the object literal pattern, but I can't seem to draw an image on the canvas. I've researched a lot and found for instance that I can't set the image src while the object is being created (so I've put that in a function). The image seems to be available as it shows up in the console and can be appended to the body. What am I missing please?
(function ($) {
$.fn.preload = function() { // http://stackoverflow.com/a/476681
this.each(function(){
$('<img/>')[0].src = this;
});
}
})(jQuery);
$(document).ready(function() {
$(['./images/grass-500x500.png']).preload(); // I've tried with and without this function (defined elsewhere)
// Object literal pattern
var game = {
// Background image
background: new Image(),
setBGSource: function() {
this.background.src = 'images/grass-500x500.png';
},
// Canvas details
canvas: $("#canvas")[0],
ctx: canvas.getContext("2d"),
WIDTH: $("#canvas").width(),
HEIGHT: $("#canvas").height(),
// Game details
CELL_WIDTH: 10,
direction: null,
food: null,
score: null,
snakeArray: [],
init: function() {
this.direction = "right";
this.createSnake();
this.setBGSource();
this.draw();
},
createSnake: function() {
var length = 5; // Initial length of snake
for (var i = length - 1; i >= 0; i--) {
this.snakeArray.push({
x: i,
y: 1 // y : 0 - initial position of snake in cell units
});
}
},
// Create food item
createFood: function() {
this.food = {
x: Math.round(Math.random() * (WIDTH - CELL_WIDTH) / CELL_WIDTH),
y: Math.round(Math.random() * (HEIGHT - CELL_WIDTH) / CELL_WIDTH)
} // Position of food with x and and y between 0 and e.g 44
},
// Drawing function
draw: function() {
// Repaint the background on each frame
console.log(this.background); // displays <img src="images/grass-500x500.png">
this.ctx.drawImage(this.background, 0, 0); // WHY DOESN'T THIS WORK PLEASE?
$('body').append(this.background); // appends image, no problem
}
};
game.init();
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="canvas"></canvas>
The problem here is that the image is being preloaded asynchronously while at the same time you call game.init() and thus game.draw(), which expects the image being loaded already.
Thus, unless the image is in your browser's cache, the asynchronous preloading might not have finished at the time of game.draw().
You need to wait until preloading has finished before calling game.init(). JavaScript offers some good tools in dealing with asynchronous execution, e.g. callbacks, promises etc.
Have a look here: https://gamedev.stackexchange.com/questions/24102/resource-loader-for-an-html5-and-javascript-game
I just started using Canvas for my web game project and faced a problem.
I'm using this code to render the game:
function render(f){
if(charoffset.x == null) charoffset.x = charpos.x*tilescale;
if(charoffset.y == null) charoffset.y = charpos.y*tilescale;
if(!tiles) tiles = [];
if(f){
log("Welcome.","gold");
}
var canPassthrough = function (){
if ((def.passable(this.type))&&(typeof this.type !== 'undefined')){
return true;
}
else {
return false;
}
};
if(!f) lighting.update();
canvas.getContext("2d").clearRect(0,0,sq,sq);
for (var i = 0; i < map[charlvl].length; i++){
if(!tiles[i]) tiles[i] = [];
for (var j = 0; j < map[charlvl][i].length; j++){
if(!tiles[i][j]) tiles[i][j] = placetile(i,j);
drawtile(tiles[i][j]);
placeitem(i,j);
}
}
ui.overlay.text("casting shadows...");
//shadowcaster(20);
var tex = document.createElement("img");
tex.src = "../img/charplaceholder.png";
var hero = canvas.getContext("2d");
hero.globalAlpha = 1.0;
if(charoffset.x>=map_scroll.x&&charoffset.y*tilescale>=map_scroll.y){
var pos = {
x: charoffset.x - map_scroll.x - tilescale,
y: charoffset.y - map_scroll.y - tilescale
};
hero.drawImage(tex,pos.x,pos.y,tilescale,tilescale);
}
function placetile(x,y){
var obj = {};
obj.type = map[charlvl][x][y].id;
obj.canPassthrough = canPassthrough;
obj.state = {explored: false, lit: false};
obj.coords = {x:x,y:y};
obj.offset = {x:x*tilescale,y:y*tilescale};
return obj;
}
function drawtile(t){
if(t.offset.x>=map_scroll.x&&t.offset.y>=map_scroll.y){
var pos = {
x: t.offset.x - map_scroll.x - tilescale,
y: t.offset.y - map_scroll.y - tilescale
};
if(!t.state.explored&&!t.state.lit){
return false;
}
else if(t.state.lit&&t.state.explored){
var tex = document.createElement("img");
var tile = canvas.getContext("2d");
tex.src = def.css.tile(t.type);
tile.globalAlpha = 1.0;
tile.drawImage(tex,pos.x,pos.y,tilescale,tilescale);
return true;
}
else if(t.state.explored&&!t.state.lit){
var tex = document.createElement("img");
var tile = canvas.getContext("2d");
tex.src = def.css.tile(t.type);
tile.globalAlpha = 0.25;
tile.drawImage(tex,pos.x,pos.y,tilescale,tilescale);
return true;
}
}
}
function placeitem(x,y){
return;
if (loot[charlvl][x][y]){
for(var i=0;i<loot[charlvl][x][y].length;i++){
var tile = document.createElement("div");
var tileid = loot[charlvl][x][y][i].type;
tile.className = def.css.item(tileid);
tile.coords = {x:x,y:y};
document.getElementById("x" + x + "y" + y).appendChild(tile);
}
}
}
if(f){
camera.center(charpos.x,charpos.y);
ui.overlay.text("loading the dungeon...");
ui.overlay.hide();
}
}
Function render() is fired by various events, such as character moving, map dragging, lighting update, etc.
This is the result:
I would like to add inset shadows to walls so it's more clearly visible those are walls. I tried experimenting with canvas context shadows, and used this:
It's supposed to draw a transparent rectangle and a shadow for it at 100, 100 with size 20, 20, however this applies shadow to every drawn tile instead.
I feel like I'm using drawing wrong. Can anyone explain how to effectively
use canvas to achieve desired effect?
Do not use the 2D API shadow options , they are very very slow ( and that is an understatement of how bad they are). You are much better off creating the shadows as part of the tile set and rendering them with either ctx.globalAlpha set to less than 1 and/or use one of the many composite modes. Eg ctx.globalCompositeOperation = "multiply"; Or overlay, color-burn, hard-light, and soft-light. You can even use a combination to get a very good shadow effect.
Creating the shadows as part of the tile set will give a much more realistic effect as the shadow API is just for shadows cast from flat object floating above a flat surface, not for 3D objects protruding from the screen that may have sloped sides in the z direction.
If you do not wish to create the shadows as part of the tile set consider creating the shadow tile set at onload using an off screen canvas via the shadow API options. Then render from that to the canvas using alpha and composite options
Is there a way to rotate the canvas in fabric.js?
I am not looking to rotate each element, that can be achieved easily, but rather a way to rotate the whole canvas similar to what is achieved with canvas.rotate() on a native canvas element:
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.rotate(20*Math.PI/180);
Accessing the canvas element from fabric.js with getContext() is possible, but if I do that and then rotate it, only one of the two canvases is being rotate and the selection/drawing is severely off and drawing/selecting/etc is not working anymore either.
I am somewhat at a loss here. If this is something that's currently not possible with fabric.js I will create a ticket on github, but somehow it feels like it should be possible ...
[edit]
After the input from Ian I've figured a few things out and am at a point where I can rotate the canvas and get some results. However, objects are very far off from the correct position. However, this might be because, while rotating, I am also zooming and absolute paning the canvas (with canvas.setZoom() and canvas.absolutePan()). I think I'll create a ticket on GitHub and see what the devs think. Somewhat stuck here ... Just for reference here's the code snippet:
setAngle: function(angle) {
var self = this;
var canvas = self.getFabricCanvas();
var group = new fabric.Group();
var origItems = canvas._objects;
var size = self.getSize();
group.set({width: size.width, height: size.height, left: size.width / 2, top: size.height / 2, originX: 'center', originY: 'center', centeredRotation: true})
for (var i = 0; i < origItems.length; i++) {
group.add(origItems[i]);
}
canvas.add(group);
group.set({angle: (-1 * self.getOldAngle())});
canvas.renderAll();
group.set({angle: angle});
canvas.renderAll();
items = group._objects;
group._restoreObjectsState();
canvas.remove(group);
for (var i = 0; i < items.length; i++) {
canvas.add(items[i]);
canvas.remove(origItems[i]);
}
canvas.renderAll();
self.setOldAngle(angle);
},
As stated above, this function is called with two other functions:
setPosition: function(left, top) {
var self = this;
if (left < 0) {
left = 0;
}
if (top < 0) {
top = 0;
}
var point = new fabric.Point(left, top);
self.getFabricCanvas().absolutePan(point);
},
setZoom: function(zoom) {
var self = this;
self.getFabricCanvas().setZoom(zoom);
},
The functions are called through the following code:
MyClass.setZoom(1);
MyClass.setPosition(left, top);
MyClass.setZoom(zoom);
MyClass.setAngle(angle);
As you can see, I try to set the angle last, but it doesn't make a difference (at least not visually) when I do that. The zoom set to 1 at the beginning is important as otherwise the panning won't work properly.
Maybe someone has an idea ...
Here is how I did this (code based on this js fiddle).
rotate (degrees) {
let canvasCenter = new fabric.Point(canvas.getWidth() / 2, canvas.getHeight() / 2) // center of canvas
let radians = fabric.util.degreesToRadians(degrees)
canvas.getObjects().forEach((obj) => {
let objectOrigin = new fabric.Point(obj.left, obj.top)
let new_loc = fabric.util.rotatePoint(objectOrigin, canvasCenter, radians)
obj.top = new_loc.y
obj.left = new_loc.x
obj.angle += degrees //rotate each object by the same angle
obj.setCoords()
});
canvas.renderAll()
},
After doing this I also had to adjust the canvas background and size so objects wouldn't go off the canvas.
I am trying to break a spritesheet and well, it's not putting it into a 2D array like it should. At the end of my code, near the console.log, it spits out ImageData...but when it tries to putImageData... I get Uncaught TypeError: Type error
Help?
and here's my code:
$(document).ready(function () {
console.log("Client setup...");
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
canvas.tabIndex = 0;
canvas.focus();
console.log("Client focused.");
var imageObj = new Image();
imageObj.src = "./images/all_tiles.png";
imageObj.onload = function() {
context.drawImage(imageObj, imageWidth, imageHeight);
};
var allLoaded = 0;
var tilesX = ImageWidth / tileWidth;
var tilesY = ImageHeight / tileHeight;
var totalTiles = tilesX * tilesY;
var tileData = [totalTiles]; // Array with reserved size
for(var i=0; i<tilesY; i++)
{
for(var j=0; j<tilesX; j++)
{
// Store the image data of each tile in the array.
tileData.push(context.getImageData(j*tileWidth, i*tileHeight, tileWidth, tileHeight));
allLoaded++;
}
}
if (allLoaded == totalTiles) {
console.log("All done: " + allLoaded);
console.log(tileData[1]);
context.putImageData(tileData[0], 0 ,0); // Sample paint
}
});
error happens at context.putImageData(tileData[0], 0 ,0); // Sample paint
Here is example fiddle of code above http://jsfiddle.net/47XUA/
Your problem is that the first element of tileData is not an ImageData object, it's a plain number! When you initialized your array you used:
var tileData = [totalTiles];
This means that tileData[0] is equal to the value of totalTiles, which is the number 483. You are providing a number instead of an ImageData object to putImageData, and the browser is throwing an error in confusion. If you look at any other element in the rest of your array, you'll see it is successfully being populated with ImageData objects as you expect; only the first element in the array is a number.
You probably meant to do:
var tileData = new Array(totalTiles);
which would create an array with the specified length property, instead of setting the first value in the array to a number. Personally, I would run some performance tests to see if that is even useful; you may be just as well using var tileData = [];
A type error is caused when a variable or object passed into a statement is not of a type that is expected for that command and can’t be converted into something that is valid.
See here for a full definition of the error.
So most likely that context or tileData is in an incorrect format or is null or undefined
It might be a racing condition. You are using drawImage only when the image ./images/all_tiles.png has been loaded. But you are using the image data on the context before the onload fired. Try to move everything which is after the onload handler into the onload handler.
The problem here is that the imageObj.onload() function is being called AFTER the rest of the script is executed.
This means that queries to getImageData() are returning undefined, as there is no image data in the canvas when called.
A simple way to fix this is to encapuslate the remaining script in the imageObj.onload function, like so:
$(document).ready(function () {
console.log("Client setup...");
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
canvas.tabIndex = 0;
canvas.focus();
console.log("Client focused.");
var imageObj = new Image();
imageObj.src = "./images/all_tiles.png";
imageObj.onload = function() {
context.drawImage(imageObj, 0, 0);
var allLoaded = 0;
var tilesX = imageWidth / tileWidth;
var tilesY = imageHeight / tileHeight;
var totalTiles = tilesX * tilesY;
var tileData = new Array(); // Array with NO reserved size
for(var i=0; i<tilesY; i++)
{
for(var j=0; j<tilesX; j++)
{
// Store the image data of each tile in the array.
tileData.push(context.getImageData(j*tileWidth, i*tileHeight, tileWidth, tileHeight));
allLoaded++;
}
}
context.putImageData(tileData[0], 0 ,0); // Sample paint
}; // End 'imageObj.onload()' scope.
}); // End document ready function scope.
This ensures that any calls to getImageData() are made after context.drawImage()
You may also have a problem here that you are not drawing the image to cover the canvas. Change the line:
context.drawImage(imageObj, imageWidth, imageHeight);
to
context.drawImage(imageObj, 0, 0);
EDIT: You also had ImageWidth and imageWidth (same for height). Rename these.
EDIT: Reserving the Array size here with new Array(totalTiles) was causing an issue. When we pushed new tile images to the array, it pushed to the end of the pre-allocated array (starting at tileData[totalTiles]). This is easily fixed by removing the size argument. Otherwise instead of using push() we could copy the image data into the array in a loop, eg: tileData[n] = getImageData(...), starting at tileData[0].
I'm using Chrome.
context.drawImage(spritesheet.image, 0, 0, 4608, 768);// Works
context.drawImage(spritesheet.image, 768, 0, 768, 768, 4608, 768); // Doesn't work: Uncaught TypeError: Type error
Thank you!