Unable to maintain proper scale while zooming using mousewheel in WebGL Magnifier - javascript

I am implementing a webgl magnifier which works fine .
But it distorts when I apply zoom using mousewheel.
I am using fabric.js and fcanvas.getZoom() returns the current zoom level that I am using to scale the magnifier.
Here's the codepen .
Can anyone check why it doesn't work while zooming in using mousewheel.
Code :
`
MainCanvas = (function() {
function MainCanvas(image, WIDTH, HEIGHT) {
var err;
this.image = image;
this.WIDTH = WIDTH != null ? WIDTH : 128;
this.HEIGHT = HEIGHT != null ? HEIGHT : 128;
this.mouseout = bind(this.mouseout, this);
this.mouseover = bind(this.mouseover, this);
this.mousemove = bind(this.mousemove, this);
this.render = bind(this.render, this);
zoomcanvas = $(document.createElement('canvas'));
zoomcanvas.css({
'position': 'absolute',
'width': this.WIDTH,
'height': this.HEIGHT,
'z-index': '1000',
'border': '3px solid rgba(0,0,0,0.4)',
'border-radius': '50%',
'box-shadow': '2px 2px 16px 2px #223',
'pointer-events': 'none'
});
$('#canvases').append(zoomcanvas);
med = [0.5, 0.5];
this.scaled = 1;
try {
this.gl = zoomcanvas[0].getContext("experimental-webgl") || zoomcanvas[0].getContext("webgl");
} catch (_error) {
err = _error;
console.log(err.message);
alert(err.message);
}
this.gl.clearColor(0.8, 0.8, 0.8, 1.0);
this.mouse = {
x: .5,
y: .5
};
this.mvMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
this.pMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
this.data = {};
this.initShaders();
this.initBuffers();
this.initImages();
this.render();
this.image.mousemove(this.mousemove);
this.image.mouseover(this.mouseover).mouseout(this.mouseout);
}
MainCanvas.prototype.initShaders = function() {
var fragmentShader, vertexShader;
fragmentShader = this.getShader('shader-fs');
vertexShader = this.getShader('shader-vs');
this.shaderProgram = this.gl.createProgram();
this.gl.attachShader(this.shaderProgram, vertexShader);
this.gl.attachShader(this.shaderProgram, fragmentShader);
this.gl.linkProgram(this.shaderProgram);
if (!this.gl.getProgramParameter(this.shaderProgram, this.gl.LINK_STATUS)) {
console.log('Não pode inicializar shaders!');
}
this.gl.useProgram(this.shaderProgram);
this.data.vertexPositionAttribute = this.gl.getAttribLocation(this.shaderProgram, "aVertexPosition");
this.gl.enableVertexAttribArray(this.data.vertexPositionAttribute);
this.data.textureCoordAttribute = this.gl.getAttribLocation(this.shaderProgram, "aTextureCoord");
this.gl.enableVertexAttribArray(this.data.textureCoordAttribute);
this.data.samplerUniform0 = this.gl.getUniformLocation(this.shaderProgram, "sampler0");
this.data.medUniform = this.gl.getUniformLocation(this.shaderProgram, "med");
this.data.scaledUniform = this.gl.getUniformLocation(this.shaderProgram, "scaled");
};
MainCanvas.prototype.setMatrixUniforms = function() {
var normalMatrix;
this.gl.uniformMatrix4fv(this.data.pMatrixUniform, false, this.pMatrix);
this.gl.uniformMatrix4fv(this.data.mvMatrixUniform, false, this.mvMatrix);
normalMatrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
return this.gl.uniformMatrix3fv(this.data.nMatrixUniform, false, normalMatrix);
};
MainCanvas.prototype.getShader = function(id) {
var k, shader, shaderScript, str;
shaderScript = document.getElementById(id);
if (shaderScript === null) {
return false;
}
str = '';
k = shaderScript.firstChild;
while (k) {
if (k.nodeType === 3) {
str += k.textContent;
}
k = k.nextSibling;
}
if (shaderScript.type === "x-shader/x-fragment") {
shader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
} else {
shader = this.gl.createShader(this.gl.VERTEX_SHADER);
}
this.gl.shaderSource(shader, str);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
console.log(this.gl.getShaderInfoLog(shader));
return false;
}
return shader;
};
MainCanvas.prototype.initBuffers = function() {
return this.bufferPlane();
};
MainCanvas.prototype.bufferPlane = function() {
var buffer, colors, indices, normais, uv, vertices;
vertices = [-1, -1, 0, 1, -1, 0, -1, 1, 0, 1, 1, 0];
buffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(vertices), this.gl.STATIC_DRAW);
buffer.itemSize = 3;
buffer.numItems = 4;
this.data.vertexPositionBuffer = buffer;
normais = [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1];
buffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(normais), this.gl.STATIC_DRAW);
buffer.itemSize = 4;
buffer.numItems = 4;
this.data.vertexColorBuffer = buffer;
uv = [0, 0, 1, 0, 0, 1, 1, 1];
buffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(uv), this.gl.STATIC_DRAW);
buffer.itemSize = 2;
buffer.numItems = 4;
this.data.vertexTextureCoordBuffer = buffer;
indices = [0, 1, 2, 2, 3, 1];
buffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, buffer);
this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), this.gl.STATIC_DRAW);
buffer.itemSize = 1;
buffer.numItems = 6;
return this.data.vertexIndexBuffer = buffer;
};
MainCanvas.prototype.handleTexture = function(texture) {
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, true);
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, canvas);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
this.gl.bindTexture(this.gl.TEXTURE_2D, null);
this.scaled = Math.max(this.WIDTH / this.image.width(), this.HEIGHT / this.image.height());
return this.scaled / (fcanvas.getZoom());
};
MainCanvas.prototype.initImages = function() {
this.data.texture0 = this.gl.createTexture();
this.handleTexture(this.data.texture0);
};
MainCanvas.prototype.render = function(t) {
this.gl.viewport(0, 0, zoomcanvas[0].width, zoomcanvas[0].height);
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
this.gl.uniform1f(this.data.scaledUniform, this.scaled / fcanvas.getZoom() / 2);
this.gl.uniform2f(this.data.medUniform, parseFloat(med[0]) , parseFloat(med[1]));
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.data.vertexPositionBuffer);
this.gl.vertexAttribPointer(this.data.vertexPositionAttribute, this.data.vertexPositionBuffer.itemSize, this.gl.FLOAT, false, 0, 0);
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.data.vertexTextureCoordBuffer);
this.gl.vertexAttribPointer(this.data.textureCoordAttribute, this.data.vertexTextureCoordBuffer.itemSize, this.gl.FLOAT, false, 0, 0);
this.gl.activeTexture(this.gl.TEXTURE0);
this.gl.bindTexture(this.gl.TEXTURE_2D, this.data.texture0);
this.gl.uniform1i(this.data.samplerUniform0, 0);
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.data.vertexIndexBuffer);
this.setMatrixUniforms();
return this.gl.drawElements(this.gl.TRIANGLES, this.data.vertexIndexBuffer.numItems, this.gl.UNSIGNED_SHORT, 0);
};
MainCanvas.prototype.mousemove = function(e) {
var o2, x, y;
o2 = this.image.offset();
x = (e.offsetX + o2.left);
y = (e.offsetY + o2.top);
zoomcanvas.css({
'left': parseInt(x - this.WIDTH * 0.5),
'top': parseInt(y - this.HEIGHT * 0.5)
});
med[0] = (((e.offsetX - imageAttrs.left) ) / (oImg.width));
med[1] = (1.0 - (((e.offsetY - imageAttrs.top)) / (oImg.height)));
$('body').css('cursor', 'none');
return this.render();
};
MainCanvas.prototype.mouseover = function(e) {
return zoomcanvas.show();
};
MainCanvas.prototype.mouseout = function(e) {
return zoomcanvas.fadeOut("fast");
};
return MainCanvas;
})();
window.requestAnimFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) {
return window.setTimeout(callback, 40);
};
window.onload = function(e) {
var mainCanvas;
console.clear();
console.log(Date.now());
var width = $("#sampler0")[0].width; var height = $("#sampler0")[0].height;
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d');
var AR = calculateAspectRatio(width, height);
ctx.drawImage($("#sampler0")[0], 0, 0, AR.renderableWidth, AR.renderableHeight);
var scaleX = AR.renderableWidth / width;
var scaleY = AR.renderableHeight / height;
oImg = new fabric.Image(canvas,
{
selectable : false,
width : width,
height : height,
lockUniScaling: true,
centeredScaling: true,
scaleX : scaleX,
scaleY : scaleY,
alignX : "mid",
alignY : "mid",
});
fcanvas.add(oImg);
///bring image to center
fcanvas.centerObject(oImg);
/////////////////////////////////////////////////////////
///scale image back to default
oImg.scaleX = 1;
oImg.scaleY = 1;
///get left and top for zooming correctly.
imageAttrs.scaleX = scaleX;
fcanvas.zoomToPoint({ x: oImg.left, y: oImg.top }, scaleX);
oImg.on('mouseout', function(event) {
zoomcanvas.fadeOut("fast");
});
oImg.on('mouseover', function(event) {
zoomcanvas.show();
});
imageAttrs.left = oImg.left;
imageAttrs.top = oImg.top;
fcanvas.renderAll();
applyZoom();
return mainCanvas = new MainCanvas($("#canvases"), 256, 256);
};
var calculateAspectRatio = function (width, height) {
var imageAspectRatio = width / height;
var canvasAspectRatio = windowWidth / windowHeight;
var renderableHeight, renderableWidth, xStart, yStart;
var AR = new Object();
/// If image's aspect ratio is less than canvas's we fit on height
/// and place the image centrally along width
if(imageAspectRatio < canvasAspectRatio) {
renderableHeight = windowHeight ;
renderableWidth = width * (renderableHeight / height);
xStart = (windowWidth - renderableWidth) / 2;
yStart = 0;
}
/// If image's aspect ratio is greater than canvas's we fit on width
/// and place the image centrally along height
else if(imageAspectRatio > canvasAspectRatio) {
renderableWidth = $(window).width()
renderableHeight = height * (renderableWidth / width);
xStart = 0;
yStart = ( windowHeight - renderableHeight) / 2;
}
///keep aspect ratio
else {
renderableHeight = windowHeight ;
renderableWidth = windowWidth;
xStart = 0;
yStart = 0;
}
AR.renderableHeight = renderableHeight;
AR.renderableWidth = renderableWidth;
return AR;
}
/**
* Used to apply Zooming on the canvas.
*/
var applyZoom = function () {
var canvasarea = document.getElementById("canvases");
if (canvasarea.addEventListener) {
// IE9, Chrome, Safari, Opera
canvasarea.addEventListener("mousewheel", zoom, false);
// Firefox
canvasarea.addEventListener("DOMMouseScroll", zoom, false);
}
// IE 6/7/8
else canvasarea.attachEvent("onmousewheel", zoom);
return this;
}
function zoom(e) {
var evt=window.event || e;
var delta = evt.detail? evt.detail*(-120) : evt.wheelDelta;
var curZoom = fcanvas.getZoom(), newZoom = curZoom + delta / 4000,
x = e.offsetX, y = e.offsetY;
//applying zoom values.
fcanvas.zoomToPoint({ x: x, y: y }, newZoom);
if(e != null)e.preventDefault();
return false;
}
}).call(this);`
Thanks

Related

Positioning 2d percpective of a 3d world on the 3d world

So I have this method to draw all bunch of rectangles in a 3d world on a 2d world, I was able to position the 3d objects on the 2d canvas just fine, but now I want to place the 2d canvas in place instead of the rectangles, (that way ill be only drawing one big rectangle with an image instead every single rectangle again)
here's the method
createBackground(objects) {
const buffer = document.createElement("canvas").getContext("2d");
const bufferRect = this.createEntity();
const display = this.display;
let {
zAxis,
canvas: {
width,
height,
},
currentCamera
} = display;
const cameraMatrix = currentCamera.matrix;
zAxis--;
const halfZ = zAxis / 2;
let { coords: [x, y], area: [w, h] } = objects[objects.length - 1];
const worldViewProjection = mat4.create();
let [bX, bY, lX, lY] = [x+w, y+h, x-w, y-h]; //big x, little x
for(let i = objects.length-1; i--;){
const object = objects[i];
const {coords: [_x, _y], area: [_w, _h]} = objects[i];
if(_x > bX){
bX = _x+_w;
} else if(_x < lX) {
lX = _x-_w;
}
if(_y > bY){
bY = _y+_h;
} else if(_y < lY) {
lY = _y-_h;
}
}
buffer.canvas.width = width;
buffer.canvas.height = height;
for (let i = objects.length; i--;) {
const {
coords: [_x, _y],
area: [_w, _h]
} = objects[i];
mat4.multiply(worldViewProjection, this.display.currentCamera.matrix, objects[i].matrix);
const points = [
[-_w / 2, -_h / 2, 0],
[ _w / 2, _h / 2, 0],
].map(p => {
const ndc = vec3.transformMat4([], p, worldViewProjection);
return [
(ndc[0] * 0.5 + 0.5) * width,
(ndc[1] * -0.5 + 0.5) * height,
];
});
const ww = points[1][0] - points[0][0];
const hh = points[1][1] - points[0][1];
buffer.strokeStyle = 'red';
buffer.strokeRect(...points[0], ww, hh);
}
bufferRect.move((bX+lX)/2, (bY+lY)/2);
bufferRect.setSize(Math.abs(bX)+Math.abs(lX)-5, Math.abs(bY)+Math.abs(lY)-5);
const texture = display.textureFromImage(buffer.canvas);
bufferRect.attachImage(texture);
}
the problem is right here where I try to position the rectangle in the middle and set its size to the distance between the far left rectangle and far right rectangle and some with top and bottom
bufferRect.move((bX+lX)/2, (bY+lY)/2);
bufferRect.setSize(Math.abs(bX)+Math.abs(lX)-5, Math.abs(bY)+Math.abs(lY)-5);
and here's the full code
const FRAGMENT_SHADER = ` precision highp float; varying highp vec2 vTextureCoord; varying lowp vec4 vColor; uniform sampler2D uSampler; uniform bool aUseText; void main(void) { if( aUseText ){ gl_FragColor = texture2D(uSampler, vTextureCoord); } else { gl_FragColor = vColor; } } `;
const VERTEX_SHADER = ` attribute vec4 aVertexPosition; attribute vec4 aVertexColor; attribute vec2 aTextureCoord; uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; uniform mat3 uTextMatrix; uniform float uPointSize; varying lowp vec4 vColor; varying highp vec2 vTextureCoord; void main(void) { gl_PointSize = uPointSize; gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; vColor = aVertexColor; vTextureCoord = (vec3(aTextureCoord, 1)*uTextMatrix).xy; } `;
function onResize(element, callback) {
let elementHeight = element.height,
elementWidth = element.width;
setInterval(function() {
if (element.height !== elementHeight || element.width !== elementWidth) {
elementHeight = element.height;
elementWidth = element.width;
callback();
}
}, 16);
}
mat4.moveToVec3 = function(out, v) {
out[12] = v[0];
out[13] = v[1];
out[14] = v[2];
};
class WebglEntity {
constructor() {
this.matrix = mat4.create();
this.coords = vec3.create();
}
translate(newCoords) {
const {
matrix,
coords
} = this;
mat4.translate(matrix, matrix, newCoords);
vec3.copy(coords, [matrix[12], matrix[13], matrix[14]]);
return this;
}
move(newCoords) {
const {
matrix,
coords
} = this;
vec3.copy(coords, newCoords);
mat4.moveToVec3(matrix, coords);
return this;
}
}
class Texture {
constructor() {
this.matrix = mat3.create();
this.image = undefined;
this.width = undefined;
this.height = undefined;
this.rotation = 0;
this.y = 0;
this.x = 0;
let onload = function() {};
Object.defineProperty(this, "onload", {
get() {
return onload;
},
set(value) {
if (this.loaded) {
value();
} else {
onload = value;
}
}
});
this.loaded = false;
}
setup(image, y, width, height, matrix, rotation) {
this.image = image;
this.y = y;
this.width = width;
this.height = height;
this.rotation = 0;
this.x = 0;
if (matrix) {
this.matrix = matrix;
if (rotation) {
this.rotation = rotation;
}
}
this.loaded = true;
}
static from(texture) {
const newTexture = new Texture();
const {
image,
y,
width,
height,
matrix,
rotation
} = texture;
newTexture.setup(image, y, width, height, mat3.clone(matrix), rotation);
return newTexture;
}
scale(w, h) {
const matrix = this.matrix;
mat3.scale(matrix, matrix, [w, h]);
}
rotate(rad) {
const matrix = this.matrix;
this.rotation = (this.rotation + rad) % (Math.PI * 2);
mat3.rotate(matrix, matrix, rad);
}
}
class Camera extends WebglEntity {
constructor(fieldOfView, aspect, zNear, zFar) {
super();
this.projection = mat4.perspective(mat4.create(), fieldOfView, aspect, zNear, zFar);
}
lookAt(lookAt) {
const {
matrix,
projection,
coords
} = this;
mat4.lookAt(matrix, coords, lookAt, [0, 1, 0]);
mat4.multiply(matrix, projection, matrix);
return this;
}
}
class Rect extends WebglEntity {
constructor() {
super();
this.positionsBuffer = undefined;
this.fragColorPos = undefined;
this.strokeColorPos = undefined;
this.strokePositionBuffer = undefined;
this.vertexAttribInfo = undefined;
this.vertextColorAttribInfo = undefined;
this.vertexCount = undefined;
this.textureInfo = undefined;
this.strokeSize = 1;
this.fillers = {
fill: false,
texture: false,
stroke: false
};
}
setup(matrix, positionsBuffer, strokePositionBuffer, vertexAttribInfo, vertextColorAttribInfo, vertexCount) {
this.matrix = matrix;
this.positionsBuffer = positionsBuffer;
this.strokePositionBuffer = strokePositionBuffer;
this.vertexAttribInfo = vertexAttribInfo;
this.vertextColorAttribInfo = vertextColorAttribInfo;
this.vertexCount = vertexCount;
return this;
}
scale(scale) {
const matrix = this.matrix;
mat4.scale(matrix, matrix, scale);
return this;
}
attachImage(newTexture) {
this.fillers.texture = true;
if (this.multiTextures) {
this.textureInfo.push(newTexture);
return;
}
this.textureInfo = newTexture;
this.fillers.TRIANGLE_STRIP = true;
return this;
}
}
class Display {
constructor(gl, programInfo, zAxis, texture) {
this.gl = gl;
this.programInfo = programInfo;
this.canvas = gl.canvas;
this.currentCamera = new Camera(45 * Math.PI / 180, gl.canvas.width / gl.canvas.height, 0.1, 100.0);
this.currentCamera.translate([0, 0, zAxis]);
this.currentCamera.lookAt([0, 0, 0]);
this.zAxis = zAxis;
this.drawZAxis = 0;
texture.textAttribInfo = {
numComponents: 2,
type: gl.FLOAT,
normalize: false,
stride: 0,
offset: 0
};
this.texture = texture;
this.spriteSheets = [];
const context = texture.context;
const canvas = texture.canvas;
this.images = {}; /*all the images with their src as their key*/
onResize(texture.canvas, () => {
const images = Object.values(this.images);
for (let i = images.length; i--;) {
const {
image,
y
} = images[i];
context.drawImage(image, 0, y);
}
const internalFormat = gl.RGBA;
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, internalFormat, gl.UNSIGNED_BYTE, canvas);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
});
}
setSize(width, height) {
const canvas = this.gl.canvas;
canvas.width = width;
canvas.height = height;
}
clear(color) {
const gl = this.gl;
gl.clearColor(0.1, 0.1, 0.3, 1);
gl.clearDepth(1.0);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
fillRect(rect, color) {
const {
createStaticDrawBuffer,
gl,
parseColor
} = this;
rect.fillers.fill = true;
if (color) {
rect.fragColorPos = createStaticDrawBuffer(gl, [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]);
}
}
textureFromImage(image, texture) {
const {
images,
texture: {
canvas
}
} = this;
texture = texture ? texture : new Texture();
const {
width,
height
} = image;
const y = canvas.height;
if (canvas.width < width) {
canvas.width = width;
}
texture.setup(image, y, width, height, 0);
canvas.height += height;
images[images.length] = texture;
return texture;
}
createRectPos(w, h) {
const rect = [w / 2, h / 2, -w / 2, h / 2, w / 2, -h / 2, -w / 2, -h / 2];
return {
rect,
stroke: undefined
};
}
getRectInfo(x, y, rect, stroke) {
return this.createSquareBuffer(rect, stroke, [x, y, this.drawZAxis]);
}
createStaticDrawBuffer(gl, data) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
return buffer;
}
createSquareBuffer(positions, strokePosition, coords) {
const {
gl,
createStaticDrawBuffer
} = this;
const positionsBuffer = createStaticDrawBuffer(gl, positions);
const strokePositionBuffer = createStaticDrawBuffer(gl, strokePosition);
const modelViewMatrix = mat4.create();
mat4.translate(modelViewMatrix, modelViewMatrix, coords);
return [modelViewMatrix, positionsBuffer, strokePositionBuffer, this.createAttribInfo(2, gl.FLOAT, false, 0, 0), this.createAttribInfo(4, gl.FLOAT, false, 0, 0), positions.length / 2];
}
createAttribInfo(numComponents, type, normalize, stride, offset) {
return {
numComponents,
type,
normalize,
stride,
offset
};
}
enableAttrib(buffer, attrib, gl, {
numComponents,
type,
normalize,
stride,
offset
}) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(attrib, numComponents, type, normalize, stride, offset);
gl.enableVertexAttribArray(attrib);
}
drawTexture(texture, gl, canvas, enableAttrib, createStaticDrawBuffer, textAttribInfo, vertexCount, textureCoord, textMatrix, useText) {
const _width = canvas.width;
const _height = canvas.height;
const {
x,
y,
width,
height,
matrix
} = texture;
const realX = x / _width;
const realWidth = realX + width / _width;
const realHeight = y / _height;
const realY = (y + height) / _height;
const fragTextPos = createStaticDrawBuffer(gl, [realWidth, realHeight, realX, realHeight, realWidth, realY, realX, realY, ]);
gl.uniformMatrix3fv(textMatrix, false, matrix);
enableAttrib(fragTextPos, textureCoord, gl, textAttribInfo);
gl.uniform1f(useText, true);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexCount);
gl.uniform1f(useText, false);
gl.disableVertexAttribArray(textureCoord);
}
drawBuffer(buffer) {
const {
gl,
drawTexture,
enableAttrib,
createStaticDrawBuffer,
currentCamera,
texture: {
context,
canvas,
textAttribInfo
},
programInfo: {
uniformLocations,
program,
attribLocations: {
vertexPosition,
vertexColor,
textureCoord
}
}
} = this;
const cameraMatrix = currentCamera.matrix;
const {
positionsBuffer,
fragColorPos,
strokeColorPos,
strokePositionBuffer,
matrix,
vertexAttribInfo,
vertextColorAttribInfo,
vertexCount,
fragTextPos,
fillers: {
fill,
stroke,
texture
},
strokeSize,
textureInfo,
multiTextures
} = buffer;
gl.uniformMatrix4fv(uniformLocations.projectionMatrix, false, cameraMatrix);
gl.uniformMatrix4fv(uniformLocations.modelViewMatrix, false, matrix);
if (fill) {
enableAttrib(positionsBuffer, vertexPosition, gl, vertexAttribInfo);
enableAttrib(fragColorPos, vertexColor, gl, vertextColorAttribInfo);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexCount);
gl.disableVertexAttribArray(vertexColor);
}
if (texture) {
const _width = canvas.width;
const _height = canvas.height;
drawTexture(textureInfo, gl, canvas, enableAttrib, createStaticDrawBuffer, textAttribInfo, vertexCount, textureCoord, uniformLocations.textMatrix, uniformLocations.useText);
}
}
static loadShader(gl, program, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
gl.attachShader(program, shader);
}
static async create(canvas, width, height, zAxis = 6) {
canvas.width = width;
canvas.height = height;
const gl = canvas.getContext("webgl");
const shaderProgram = gl.createProgram();
Display.loadShader(gl, shaderProgram, gl.VERTEX_SHADER, VERTEX_SHADER);
Display.loadShader(gl, shaderProgram, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
gl.linkProgram(shaderProgram);
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'),
textureCoord: gl.getAttribLocation(shaderProgram, 'aTextureCoord'),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
textMatrix: gl.getUniformLocation(shaderProgram, 'uTextMatrix'),
sampler: gl.getUniformLocation(shaderProgram, 'uSampler'),
useText: gl.getUniformLocation(shaderProgram, 'aUseText'),
pointSize: gl.getUniformLocation(shaderProgram, 'uPointSize'),
},
};
gl.useProgram(programInfo.program);
gl.uniform1f(programInfo.uniformLocations.pointSize, 1.0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
const textureBuffer = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textureBuffer);
gl.uniform1i(programInfo.uniformLocations.uSampler, 0);
const textureCanvas = document.createElement("canvas");
textureCanvas.width = 0;
textureCanvas.height = 0;
let texture = {
canvas: textureCanvas,
buffer: textureBuffer,
context: textureCanvas.getContext("2d"),
};
return new Display(gl, programInfo, zAxis, texture);
}
}
class Entity extends Rect {
constructor() {
super();
this.velocity = vec2.create();
this.area = undefined;
this.mass = 2;
this.updateFillers = {};
this.delete = false;
this.draw = true;
}
setup(w, h, ...args) {
this.area = vec2.fromValues(w, h);
super.setup(...args);
return this;
}
fill(...args) {
this.updateFillers.fill = args;
}
attachImage(image) {
super.attachImage(image);
}
move(x, y) {
super.move([x, y, this.coords[2]]);
return this;
}
setSize(w, h) {
if (typeof w == "object") {
h = w[1];
w = w[0];
}
const area = this.area;
const [_w, _h] = area;
super.scale([w / _w, h / _h, 1]);
area[0] = w;
area[1] = h;
return this;
}
}
class Engine {
constructor(time_step, update, render, allowedSkippedFrames) {
this.accumulated_time = 0;
this.animation_frame_request = undefined, this.time = undefined, this.time_step = time_step, this.updated = false;
this.update = update;
this.render = render;
this.allowedSkippedFrames = allowedSkippedFrames;
this.run = this.run.bind(this);
this.end = false;
}
run(time_stamp) {
const {
accumulated_time,
time,
time_step,
updated,
update,
render,
allowedSkippedFrames,
end
} = this;
this.accumulated_time += time_stamp - time;
this.time = time_stamp;
update(time_stamp);
render(time_stamp);
if (end) {
return;
}
this.animation_frame_request = requestAnimationFrame(this.run);
}
start() {
this.accumulated_time = this.time_step;
this.time = performance.now();
this.animation_frame_request = requestAnimationFrame(this.run);
}
stop() {
this.end = true;
cancelAnimationFrame(this.animation_frame_request);
}
}
const engineMath = {
randomBetween: function(min, max) {
return min + Math.random() * (max - min);
},
};
class Quixotic {
constructor(display) {
this.display = display;
this.engine = undefined;
this.render = undefined;
this.update = undefined;
this.frameRate = undefined;
this.time = 0;
this.speed = 1;
this.world = {
objects: {},
objectsCollisionInfo: {},
objectsArray: [],
classesInfo: {}
};
this.timePassed = 0;
}
createEntity(Class, ...args) {
const display = this.display;
const {
rect,
stroke
} = display.createRectPos(5, 5);
let instance = new Entity();
instance.setup(5, 5, ...display.getRectInfo(0, 0, rect, stroke, "#000"));
this.world.objectsArray.push(instance);
return instance;
}
createBackground(objects) {
const buffer = document.createElement("canvas").getContext("2d");
const bufferRect = this.createEntity();
const display = this.display;
let {
zAxis,
canvas: {
width,
height,
},
currentCamera
} = display;
const cameraMatrix = currentCamera.matrix;
zAxis--;
const halfZ = zAxis / 2;
let {
coords: [x, y],
area: [w, h]
} = objects[objects.length - 1];
const worldViewProjection = mat4.create();
let [bX, bY, lX, lY] = [x + w, y + h, x - w, y - h]; //big x, little x
for (let i = objects.length - 1; i--;) {
const object = objects[i];
const {
coords: [_x, _y],
area: [_w, _h]
} = objects[i];
if (_x > bX) {
bX = _x + _w;
} else if (_x < lX) {
lX = _x - _w;
}
if (_y > bY) {
bY = _y + _h;
} else if (_y < lY) {
lY = _y - _h;
}
}
buffer.canvas.width = width;
buffer.canvas.height = height;
for (let i = objects.length; i--;) {
const {
coords: [_x, _y],
area: [_w, _h]
} = objects[i];
mat4.multiply(worldViewProjection, this.display.currentCamera.matrix, objects[i].matrix);
const points = [
[-_w / 2, -_h / 2, 0],
[_w / 2, _h / 2, 0],
].map(p => {
const ndc = vec3.transformMat4([], p, worldViewProjection);
return [
(ndc[0] * 0.5 + 0.5) * width,
(ndc[1] * -0.5 + 0.5) * height,
];
});
const ww = points[1][0] - points[0][0];
const hh = points[1][1] - points[0][1];
buffer.strokeStyle = 'red';
buffer.strokeRect(...points[0], ww, hh);
}
bufferRect.move((bX + lX) / 2, (bY + lY) / 2);
bufferRect.setSize(Math.abs(bX) + Math.abs(lX) - 5, Math.abs(bY) + Math.abs(lY) - 5);
const texture = display.textureFromImage(buffer.canvas);
bufferRect.attachImage(texture);
}
buildWorld({
objects,
classes,
tileMap
}) {
const world = this.world;
if (Array.isArray(objects)) {
for (let i = objects.length - 1; i > -1; i--) {
const object = objects[i];
const {
amount,
position,
} = object;
const {
rangeX,
rangeY
} = position;
let _array = [];
for (let j = amount; j--;) {
const instance = this.createEntity();
instance.move(engineMath.randomBetween(...rangeX), engineMath.randomBetween(...rangeY));
_array.push(instance);
}
world.objects[name] = _array;
world.objectsArray.push(..._array);
}
}
return;
}
setup(game) {
const {
style: {
backgroundColor,
backgroundImage,
stroke
},
world,
engine: {
frameRate,
update,
render
},
setup
} = game;
this.buildWorld(world);
const {
display,
world: {
objectsArray,
objects
}
} = this;
this.frameRate = frameRate;
let lastUpdated = 0;
this.update = (time) => {
let deltaTime = time - lastUpdated;
lastUpdated = time;
const speed = this.speed;
this.timePassed += deltaTime * speed;
update(deltaTime / 1000, this);
};
let lastRendered = 0;
this.render = (timeStamp) => {
if (backgroundColor) display.clear(backgroundColor);
const length = objectsArray.length;
for (let i = length; i--;) {
const object = objectsArray[length - i - 1];
if (object.draw) {
const updateFillers = Object.entries(object.updateFillers);
const fillersLength = updateFillers.length;
if (fillersLength) {
for (let j = fillersLength; j--;) {
const [func, args] = updateFillers[fillersLength - j - 1];
display[func + "Rect"](object, ...args);
}
object.updateFillers = {};
}
display.drawBuffer(object);
}
}
render(display, this);
};
setup(this, this.world);
this.engine = new Engine(this.frameRate, this.update, this.render, 3);
this.engine.start();
return game;
}
static async create({
display: {
canvas,
width,
height,
zAxis
},
homeURL
}) {
const display = await Display.create(canvas, width, height, zAxis);
return new Quixotic(display);
}
}
const fps = document.querySelector("#fps");
const minLength = innerWidth > innerHeight ? innerHeight : innerWidth;
const game = {
create: {
display: {
canvas: document.querySelector("#canvas"),
zAxis: 90,
width: minLength,
height: minLength,
},
homeURL: "/src"
},
style: {
backgroundColor: "#111122"
},
world: {
objects: [{
name: "trees",
array: true,
amount: 5,
position: {
type: "random",
rangeX: [-37.5, 37.5],
rangeY: [-37.5, 37.5]
}
}]
},
engine: {
frameRate: 1000 / 30,
update: function(deltaTime, engine) {
fps.innerText = 1 / deltaTime;
},
render: function(display) {}
},
setup: function(engine, {
objectsArray
}) {
objectsArray.forEach(tree => {
tree.fill("#00ff00")
})
engine.createBackground(objectsArray);
}
};
Quixotic.create(game.create)
.then(engine => {
engine.setup(game);
});
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #111c31;
overflow: hidden;
align-items: space-around;
display: grid;
height: 100%;
width: 100%;
}
#canvas {
background-color: #152646;
/* justify-self: center; */
}
#fps {
position: fixed;
color: white;
right: 0;
}
canvas {
position: fixed
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>
<canvas id="canvas" width="300" height="300"></canvas>
<p id="fps"></p>
You compute affine coordinates to a perspective projection. For this the corners of the rectangles are transformed by the view matrix and projection matrix. That seems to work fine, but you missed to change the projection and view matrix when the red rectangles are drawn. Hence the vertex coordinates are transformed twice by the projection and view matrix. First when the affine coordinates are computed and 2nd time in the vertex shader.
When you draw the red rectangles then you have to set an orthographic projection matrix that matches the unit of the red rectangles, and view matrix has to be the Identity matrix.
The red rectangles are drawn to a plane in a coordinated system where an area with the size of 5x5 is mapped on the viewport. The center is at (0, 0). Hence the orthographic projection matrix is
[2/5,0,0,0, 0,2/5,0,0, 0,0,1,0, 0,0,0,1]
Change the view matrix and projection matrix after the green objects have been drawn:
gl.uniformMatrix4fv(uniformLocations.projectionMatrix, false, cameraMatrix);
gl.uniformMatrix4fv(uniformLocations.modelViewMatrix, false, matrix);
if (fill) {
enableAttrib(positionsBuffer, vertexPosition, gl, vertexAttribInfo);
enableAttrib(fragColorPos, vertexColor, gl, vertextColorAttribInfo);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexCount);
gl.disableVertexAttribArray(vertexColor);
}
const ortho_mat = [2/5,0,0,0, 0,2/5,0,0, 0,0,1,0, 0,0,0,1];
const identity_mat = [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1];
gl.uniformMatrix4fv(uniformLocations.projectionMatrix, false, ortho_mat);
gl.uniformMatrix4fv(uniformLocations.modelViewMatrix, false, identity_mat);
// [...]
See the example:
const FRAGMENT_SHADER = ` precision highp float; varying highp vec2 vTextureCoord; varying lowp vec4 vColor; uniform sampler2D uSampler; uniform bool aUseText; void main(void) { if( aUseText ){ gl_FragColor = texture2D(uSampler, vTextureCoord); } else { gl_FragColor = vColor; } } `;
const VERTEX_SHADER = ` attribute vec4 aVertexPosition; attribute vec4 aVertexColor; attribute vec2 aTextureCoord; uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; uniform mat3 uTextMatrix; uniform float uPointSize; varying lowp vec4 vColor; varying highp vec2 vTextureCoord; void main(void) { gl_PointSize = uPointSize; gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; vColor = aVertexColor; vTextureCoord = (vec3(aTextureCoord, 1)*uTextMatrix).xy; } `;
function onResize(element, callback) {
let elementHeight = element.height,
elementWidth = element.width;
setInterval(function() {
if (element.height !== elementHeight || element.width !== elementWidth) {
elementHeight = element.height;
elementWidth = element.width;
callback();
}
}, 16);
}
mat4.moveToVec3 = function(out, v) {
out[12] = v[0];
out[13] = v[1];
out[14] = v[2];
};
class WebglEntity {
constructor() {
this.matrix = mat4.create();
this.coords = vec3.create();
}
translate(newCoords) {
const {
matrix,
coords
} = this;
mat4.translate(matrix, matrix, newCoords);
vec3.copy(coords, [matrix[12], matrix[13], matrix[14]]);
return this;
}
move(newCoords) {
const {
matrix,
coords
} = this;
vec3.copy(coords, newCoords);
mat4.moveToVec3(matrix, coords);
return this;
}
}
class Texture {
constructor() {
this.matrix = mat3.create();
this.image = undefined;
this.width = undefined;
this.height = undefined;
this.rotation = 0;
this.y = 0;
this.x = 0;
let onload = function() {};
Object.defineProperty(this, "onload", {
get() {
return onload;
},
set(value) {
if (this.loaded) {
value();
} else {
onload = value;
}
}
});
this.loaded = false;
}
setup(image, y, width, height, matrix, rotation) {
this.image = image;
this.y = y;
this.width = width;
this.height = height;
this.rotation = 0;
this.x = 0;
if (matrix) {
this.matrix = matrix;
if (rotation) {
this.rotation = rotation;
}
}
this.loaded = true;
}
static from(texture) {
const newTexture = new Texture();
const {
image,
y,
width,
height,
matrix,
rotation
} = texture;
newTexture.setup(image, y, width, height, mat3.clone(matrix), rotation);
return newTexture;
}
scale(w, h) {
const matrix = this.matrix;
mat3.scale(matrix, matrix, [w, h]);
}
rotate(rad) {
const matrix = this.matrix;
this.rotation = (this.rotation + rad) % (Math.PI * 2);
mat3.rotate(matrix, matrix, rad);
}
}
class Camera extends WebglEntity {
constructor(fieldOfView, aspect, zNear, zFar) {
super();
this.projection = mat4.perspective(mat4.create(), fieldOfView, aspect, zNear, zFar);
}
lookAt(lookAt) {
const {
matrix,
projection,
coords
} = this;
mat4.lookAt(matrix, coords, lookAt, [0, 1, 0]);
mat4.multiply(matrix, projection, matrix);
return this;
}
}
class Rect extends WebglEntity {
constructor() {
super();
this.positionsBuffer = undefined;
this.fragColorPos = undefined;
this.strokeColorPos = undefined;
this.strokePositionBuffer = undefined;
this.vertexAttribInfo = undefined;
this.vertextColorAttribInfo = undefined;
this.vertexCount = undefined;
this.textureInfo = undefined;
this.strokeSize = 1;
this.fillers = {
fill: false,
texture: false,
stroke: false
};
}
setup(matrix, positionsBuffer, strokePositionBuffer, vertexAttribInfo, vertextColorAttribInfo, vertexCount) {
this.matrix = matrix;
this.positionsBuffer = positionsBuffer;
this.strokePositionBuffer = strokePositionBuffer;
this.vertexAttribInfo = vertexAttribInfo;
this.vertextColorAttribInfo = vertextColorAttribInfo;
this.vertexCount = vertexCount;
return this;
}
scale(scale) {
const matrix = this.matrix;
mat4.scale(matrix, matrix, scale);
return this;
}
attachImage(newTexture) {
this.fillers.texture = true;
if (this.multiTextures) {
this.textureInfo.push(newTexture);
return;
}
this.textureInfo = newTexture;
this.fillers.TRIANGLE_STRIP = true;
return this;
}
}
class Display {
constructor(gl, programInfo, zAxis, texture) {
this.gl = gl;
this.programInfo = programInfo;
this.canvas = gl.canvas;
this.currentCamera = new Camera(45 * Math.PI / 180, gl.canvas.width / gl.canvas.height, 0.1, 100.0);
this.currentCamera.translate([0, 0, zAxis]);
this.currentCamera.lookAt([0, 0, 0]);
this.zAxis = zAxis;
this.drawZAxis = 0;
texture.textAttribInfo = {
numComponents: 2,
type: gl.FLOAT,
normalize: false,
stride: 0,
offset: 0
};
this.texture = texture;
this.spriteSheets = [];
const context = texture.context;
const canvas = texture.canvas;
this.images = {}; /*all the images with their src as their key*/
onResize(texture.canvas, () => {
const images = Object.values(this.images);
for (let i = images.length; i--;) {
const {
image,
y
} = images[i];
context.drawImage(image, 0, y);
}
const internalFormat = gl.RGBA;
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, internalFormat, gl.UNSIGNED_BYTE, canvas);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
});
}
setSize(width, height) {
const canvas = this.gl.canvas;
canvas.width = width;
canvas.height = height;
}
clear(color) {
const gl = this.gl;
gl.clearColor(0.1, 0.1, 0.3, 1);
gl.clearDepth(1.0);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
fillRect(rect, color) {
const {
createStaticDrawBuffer,
gl,
parseColor
} = this;
rect.fillers.fill = true;
if (color) {
rect.fragColorPos = createStaticDrawBuffer(gl, [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]);
}
}
textureFromImage(image, texture) {
const {
images,
texture: {
canvas
}
} = this;
texture = texture ? texture : new Texture();
const {
width,
height
} = image;
const y = canvas.height;
if (canvas.width < width) {
canvas.width = width;
}
texture.setup(image, y, width, height, 0);
canvas.height += height;
images[images.length] = texture;
return texture;
}
createRectPos(w, h) {
const rect = [w / 2, h / 2, -w / 2, h / 2, w / 2, -h / 2, -w / 2, -h / 2];
return {
rect,
stroke: undefined
};
}
getRectInfo(x, y, rect, stroke) {
return this.createSquareBuffer(rect, stroke, [x, y, this.drawZAxis]);
}
createStaticDrawBuffer(gl, data) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
return buffer;
}
createSquareBuffer(positions, strokePosition, coords) {
const {
gl,
createStaticDrawBuffer
} = this;
const positionsBuffer = createStaticDrawBuffer(gl, positions);
const strokePositionBuffer = createStaticDrawBuffer(gl, strokePosition);
const modelViewMatrix = mat4.create();
mat4.translate(modelViewMatrix, modelViewMatrix, coords);
return [modelViewMatrix, positionsBuffer, strokePositionBuffer, this.createAttribInfo(2, gl.FLOAT, false, 0, 0), this.createAttribInfo(4, gl.FLOAT, false, 0, 0), positions.length / 2];
}
createAttribInfo(numComponents, type, normalize, stride, offset) {
return {
numComponents,
type,
normalize,
stride,
offset
};
}
enableAttrib(buffer, attrib, gl, {
numComponents,
type,
normalize,
stride,
offset
}) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(attrib, numComponents, type, normalize, stride, offset);
gl.enableVertexAttribArray(attrib);
}
drawTexture(texture, gl, canvas, enableAttrib, createStaticDrawBuffer, textAttribInfo, vertexCount, textureCoord, textMatrix, useText) {
const _width = canvas.width;
const _height = canvas.height;
const {
x,
y,
width,
height,
matrix
} = texture;
const realX = x / _width;
const realWidth = realX + width / _width;
const realHeight = y / _height;
const realY = (y + height) / _height;
const fragTextPos = createStaticDrawBuffer(gl, [realWidth, realHeight, realX, realHeight, realWidth, realY, realX, realY, ]);
gl.uniformMatrix3fv(textMatrix, false, matrix);
enableAttrib(fragTextPos, textureCoord, gl, textAttribInfo);
gl.uniform1f(useText, true);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexCount);
gl.uniform1f(useText, false);
gl.disableVertexAttribArray(textureCoord);
}
drawBuffer(buffer) {
const {
gl,
drawTexture,
enableAttrib,
createStaticDrawBuffer,
currentCamera,
texture: {
context,
canvas,
textAttribInfo
},
programInfo: {
uniformLocations,
program,
attribLocations: {
vertexPosition,
vertexColor,
textureCoord
}
}
} = this;
const cameraMatrix = currentCamera.matrix;
const {
positionsBuffer,
fragColorPos,
strokeColorPos,
strokePositionBuffer,
matrix,
vertexAttribInfo,
vertextColorAttribInfo,
vertexCount,
fragTextPos,
fillers: {
fill,
stroke,
texture
},
strokeSize,
textureInfo,
multiTextures
} = buffer;
gl.uniformMatrix4fv(uniformLocations.projectionMatrix, false, cameraMatrix);
gl.uniformMatrix4fv(uniformLocations.modelViewMatrix, false, matrix);
if (fill) {
enableAttrib(positionsBuffer, vertexPosition, gl, vertexAttribInfo);
enableAttrib(fragColorPos, vertexColor, gl, vertextColorAttribInfo);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexCount);
gl.disableVertexAttribArray(vertexColor);
}
const ortho_mat = [2/5,0,0,0, 0,2/5,0,0, 0,0,1,0, 0,0,0,1];
const identity_mat = [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1];
gl.uniformMatrix4fv(uniformLocations.projectionMatrix, false, ortho_mat);
gl.uniformMatrix4fv(uniformLocations.modelViewMatrix, false, identity_mat);
if (texture) {
const _width = canvas.width;
const _height = canvas.height;
drawTexture(textureInfo, gl, canvas, enableAttrib, createStaticDrawBuffer, textAttribInfo, vertexCount, textureCoord, uniformLocations.textMatrix, uniformLocations.useText);
}
}
static loadShader(gl, program, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
gl.attachShader(program, shader);
}
static async create(canvas, width, height, zAxis = 6) {
canvas.width = width;
canvas.height = height;
const gl = canvas.getContext("webgl");
const shaderProgram = gl.createProgram();
Display.loadShader(gl, shaderProgram, gl.VERTEX_SHADER, VERTEX_SHADER);
Display.loadShader(gl, shaderProgram, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
gl.linkProgram(shaderProgram);
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'),
textureCoord: gl.getAttribLocation(shaderProgram, 'aTextureCoord'),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
textMatrix: gl.getUniformLocation(shaderProgram, 'uTextMatrix'),
sampler: gl.getUniformLocation(shaderProgram, 'uSampler'),
useText: gl.getUniformLocation(shaderProgram, 'aUseText'),
pointSize: gl.getUniformLocation(shaderProgram, 'uPointSize'),
},
};
gl.useProgram(programInfo.program);
gl.uniform1f(programInfo.uniformLocations.pointSize, 1.0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
const textureBuffer = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textureBuffer);
gl.uniform1i(programInfo.uniformLocations.uSampler, 0);
const textureCanvas = document.createElement("canvas");
textureCanvas.width = 0;
textureCanvas.height = 0;
let texture = {
canvas: textureCanvas,
buffer: textureBuffer,
context: textureCanvas.getContext("2d"),
};
return new Display(gl, programInfo, zAxis, texture);
}
}
class Entity extends Rect {
constructor() {
super();
this.velocity = vec2.create();
this.area = undefined;
this.mass = 2;
this.updateFillers = {};
this.delete = false;
this.draw = true;
}
setup(w, h, ...args) {
this.area = vec2.fromValues(w, h);
super.setup(...args);
return this;
}
fill(...args) {
this.updateFillers.fill = args;
}
attachImage(image) {
super.attachImage(image);
}
move(x, y) {
super.move([x, y, this.coords[2]]);
return this;
}
setSize(w, h) {
if (typeof w == "object") {
h = w[1];
w = w[0];
}
const area = this.area;
const [_w, _h] = area;
super.scale([w / _w, h / _h, 1]);
area[0] = w;
area[1] = h;
return this;
}
}
class Engine {
constructor(time_step, update, render, allowedSkippedFrames) {
this.accumulated_time = 0;
this.animation_frame_request = undefined, this.time = undefined, this.time_step = time_step, this.updated = false;
this.update = update;
this.render = render;
this.allowedSkippedFrames = allowedSkippedFrames;
this.run = this.run.bind(this);
this.end = false;
}
run(time_stamp) {
const {
accumulated_time,
time,
time_step,
updated,
update,
render,
allowedSkippedFrames,
end
} = this;
this.accumulated_time += time_stamp - time;
this.time = time_stamp;
update(time_stamp);
render(time_stamp);
if (end) {
return;
}
this.animation_frame_request = requestAnimationFrame(this.run);
}
start() {
this.accumulated_time = this.time_step;
this.time = performance.now();
this.animation_frame_request = requestAnimationFrame(this.run);
}
stop() {
this.end = true;
cancelAnimationFrame(this.animation_frame_request);
}
}
const engineMath = {
randomBetween: function(min, max) {
return min + Math.random() * (max - min);
},
};
class Quixotic {
constructor(display) {
this.display = display;
this.engine = undefined;
this.render = undefined;
this.update = undefined;
this.frameRate = undefined;
this.time = 0;
this.speed = 1;
this.world = {
objects: {},
objectsCollisionInfo: {},
objectsArray: [],
classesInfo: {}
};
this.timePassed = 0;
}
createEntity(Class, ...args) {
const display = this.display;
const {
rect,
stroke
} = display.createRectPos(5, 5);
let instance = new Entity();
instance.setup(5, 5, ...display.getRectInfo(0, 0, rect, stroke, "#000"));
this.world.objectsArray.push(instance);
return instance;
}
createBackground(objects) {
const buffer = document.createElement("canvas").getContext("2d");
const bufferRect = this.createEntity();
const display = this.display;
let {
zAxis,
canvas: {
width,
height,
},
currentCamera
} = display;
const cameraMatrix = currentCamera.matrix;
zAxis--;
const halfZ = zAxis / 2;
let {
coords: [x, y],
area: [w, h]
} = objects[objects.length - 1];
const worldViewProjection = mat4.create();
let [bX, bY, lX, lY] = [x + w, y + h, x - w, y - h]; //big x, little x
for (let i = objects.length - 1; i--;) {
const object = objects[i];
const {
coords: [_x, _y],
area: [_w, _h]
} = objects[i];
if (_x > bX) {
bX = (_x + _w);
} else if (_x < lX) {
lX = _x - _w;
}
if (_y > bY) {
bY = _y + _h;
} else if (_y < lY) {
lY = _y - _h;
}
}
buffer.canvas.width = width;
buffer.canvas.height = height;
for (let i = objects.length; i--;) {
const {
coords: [_x, _y],
area: [_w, _h]
} = objects[i];
mat4.multiply(worldViewProjection, this.display.currentCamera.matrix, objects[i].matrix);
const points = [
[-_w / 2, -_h / 2, 0],
[_w / 2, _h / 2, 0],
].map(p => {
const ndc = vec3.transformMat4([], p, worldViewProjection);
return [
(ndc[0] * 0.5 + 0.5) * width,
(ndc[1] * -0.5 + 0.5) * height,
];
});
const ww = points[1][0] - points[0][0];
const hh = points[1][1] - points[0][1];
buffer.strokeStyle = 'red';
buffer.strokeRect(...points[0], ww, hh);
}
bufferRect.move((bX + lX) / 2, (bY + lY) / 2);
bufferRect.setSize(Math.abs(bX) + Math.abs(lX) - 5, Math.abs(bY) + Math.abs(lY) - 5);
const texture = display.textureFromImage(buffer.canvas);
bufferRect.attachImage(texture);
}
buildWorld({
objects,
classes,
tileMap
}) {
const world = this.world;
if (Array.isArray(objects)) {
for (let i = objects.length - 1; i > -1; i--) {
const object = objects[i];
const {
amount,
position,
} = object;
const {
rangeX,
rangeY
} = position;
let _array = [];
for (let j = amount; j--;) {
const instance = this.createEntity();
instance.move(engineMath.randomBetween(...rangeX), engineMath.randomBetween(...rangeY));
_array.push(instance);
}
world.objects[name] = _array;
world.objectsArray.push(..._array);
}
}
return;
}
setup(game) {
const {
style: {
backgroundColor,
backgroundImage,
stroke
},
world,
engine: {
frameRate,
update,
render
},
setup
} = game;
this.buildWorld(world);
const {
display,
world: {
objectsArray,
objects
}
} = this;
this.frameRate = frameRate;
let lastUpdated = 0;
this.update = (time) => {
let deltaTime = time - lastUpdated;
lastUpdated = time;
const speed = this.speed;
this.timePassed += deltaTime * speed;
update(deltaTime / 1000, this);
};
let lastRendered = 0;
this.render = (timeStamp) => {
if (backgroundColor) display.clear(backgroundColor);
const length = objectsArray.length;
for (let i = length; i--;) {
const object = objectsArray[length - i - 1];
if (object.draw) {
const updateFillers = Object.entries(object.updateFillers);
const fillersLength = updateFillers.length;
if (fillersLength) {
for (let j = fillersLength; j--;) {
const [func, args] = updateFillers[fillersLength - j - 1];
display[func + "Rect"](object, ...args);
}
object.updateFillers = {};
}
display.drawBuffer(object);
}
}
render(display, this);
};
setup(this, this.world);
this.engine = new Engine(this.frameRate, this.update, this.render, 3);
this.engine.start();
return game;
}
static async create({
display: {
canvas,
width,
height,
zAxis
},
homeURL
}) {
const display = await Display.create(canvas, width, height, zAxis);
return new Quixotic(display);
}
}
const fps = document.querySelector("#fps");
const minLength = innerWidth > innerHeight ? innerHeight : innerWidth;
const game = {
create: {
display: {
canvas: document.querySelector("#canvas"),
zAxis: 90,
width: minLength,
height: minLength,
},
homeURL: "/src"
},
style: {
backgroundColor: "#111122"
},
world: {
objects: [{
name: "trees",
array: true,
amount: 5,
position: {
type: "random",
rangeX: [-37.5, 37.5],
rangeY: [-37.5, 37.5]
}
}]
},
engine: {
frameRate: 1000 / 30,
update: function(deltaTime, engine) {
fps.innerText = 1 / deltaTime;
},
render: function(display) {}
},
setup: function(engine, {
objectsArray
}) {
objectsArray.forEach(tree => {
tree.fill("#00ff00")
})
engine.createBackground(objectsArray);
}
};
Quixotic.create(game.create)
.then(engine => {
engine.setup(game);
});
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background-color: #111c31; overflow: hidden; align-items: space-around; display: grid; height: 100%; width: 100%; }
#canvas { background-color: #152646; /* justify-self: center; */}
#fps { position: fixed; color: white; right: 0; }
canvas { position: fixed }
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>
<canvas id="canvas" width="300" height="300"></canvas>
<p id="fps"></p>

convolve kernal matrics in javascript for image filter in html5canvas

I have been trying to learn filters in javascript, i have been following
https://www.html5rocks.com/en/tutorials/canvas/imagefilters/
this tutorial.
I came across some of code i don't get, can some body help me understanding these codes.
Filters.convolute = function(pixels, weights, opaque) {
var side = Math.round(Math.sqrt(weights.length));
var halfSide = Math.floor(side/2);
var src = pixels.data;
var sw = pixels.width;
var sh = pixels.height;
// pad output by the convolution matrix
var w = sw;
var h = sh;
var output = Filters.createImageData(w, h);
var dst = output.data;
// go through the destination image pixels
var alphaFac = opaque ? 1 : 0;
for (var y=0; y<h; y++) {
for (var x=0; x<w; x++) {
var sy = y;
var sx = x;
var dstOff = (y*w+x)*4;
// calculate the weighed sum of the source image pixels that
// fall under the convolution matrix
var r=0, g=0, b=0, a=0;
for (var cy=0; cy<side; cy++) {
for (var cx=0; cx<side; cx++) {
var scy = sy + cy - halfSide;
var scx = sx + cx - halfSide;
if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) {
var srcOff = (scy*sw+scx)*4;
var wt = weights[cy*side+cx];
r += src[srcOff] * wt;
g += src[srcOff+1] * wt;
b += src[srcOff+2] * wt;
a += src[srcOff+3] * wt;
}
}
}
dst[dstOff] = r;
dst[dstOff+1] = g;
dst[dstOff+2] = b;
dst[dstOff+3] = a + alphaFac*(255-a);
}
}
return output;
};
what is side and halfSide and why 4 for nested loop is used for. i am stuck here like many days.
I do, like you the same thing, I am trying to implement convolution filters using Javascript - TypeScript.
The reason why is 4 is because we have r, g, b, a
where r = red
where g = green
where b = blue
where a = alpha
this image data is inside an array of type Uint8ClampedArray
you get this information with this way:
const width = canvas.width;
const height = canvas.height;
const imageData = ctx.getImageData(0, 0, width, height);
and then to get the real image data:
const pixels = imageData.data;
The pixel data is a type of Uint8ClampedArray and can be represented like this:
[r, g, b, a, r, g, b, a, r, g, b, a ]
and every 4 elements in the array you get the pixel index and every 1,5 times you get the kernel Center but this depends on the kernel size 3x3 or 9x9
const image = imageData.data
The only code is working for me is this.
init() {
const img = new Image();
const img2 = new Image();
img.src = '../../../assets/graffiti.jpg';
img2.src = '../../../assets/graffiti.jpg';
const canvas: HTMLCanvasElement = this.canvas1.nativeElement;
const canvas2: HTMLCanvasElement = this.canvas2.nativeElement;
const ctx: CanvasRenderingContext2D = canvas.getContext('2d');
const ctx2: CanvasRenderingContext2D = canvas2.getContext('2d');
this.onImgLoad(img, ctx, canvas.width, canvas.height);
this.input(img2, ctx2, canvas2.width, canvas2.height);
}
input(img, ctx: CanvasRenderingContext2D, width, height) {
img.onload = () => {
ctx.drawImage(img, 0, 0);
};
}
onImgLoad(img, ctx: CanvasRenderingContext2D, width, height) {
img.onload = () => {
ctx.drawImage(img, 0, 0);
const kernelArr = new Kernel([
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
]);
const kernel = [
0, 1, 0,
0, 1, 0,
0, 1, 0
];
console.log(kernel);
const newImg = new Filter2D(ctx, width, height);
// const imgData = newImg.inverse(width, height); // applys inverse filter
const imgData = newImg.applyKernel(kernel);
ctx.putImageData(imgData, 0, 0);
};
}
class Filter2D {
width: number;
height: number;
ctx: CanvasRenderingContext2D;
imgData: ImageData;
constructor(ctx: CanvasRenderingContext2D, width: number, height: number) {
this.width = width;
this.height = height;
this.ctx = ctx;
this.imgData = ctx.getImageData(0, 0, width, height);
console.log(this.imgData);
}
grey(width: number, height: number): ImageData {
return this.imgData;
}
inverse(width: number, height: number): ImageData {
console.log('Width: ', width);
console.log('Height: ', height);
const pixels = this.imgData.data;
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = 255 - pixels[i]; // red
pixels[i + 1] = 255 - pixels[i + 1]; // green
pixels[i + 2] = 255 - pixels[i + 2]; // blue
}
return this.imgData;
}
applyKernel(kernel: any[]): ImageData {
const k1: number[] = [
1, 0, -1,
2, 0, -2,
1, 0, -1
];
const k2: number[] = [
-1, -1, -1,
-1, 8, -1,
-1, -1, -1
];
kernel = k2;
const dim = Math.sqrt(kernel.length);
const pad = Math.floor(dim / 2);
const pixels: Uint8ClampedArray = this.imgData.data;
const width: number = this.imgData.width;
const height: number = this.imgData.height;
console.log(this.imgData);
console.log('applyKernelMethod start');
console.log('Width: ', width);
console.log('Height: ', height);
console.log('kernel: ', kernel);
console.log('dim: ', dim); // 3
console.log('pad: ', pad); // 1
console.log('dim % 2: ', dim % 2); // 1
console.log('pixels: ', pixels);
if (dim % 2 !== 1) {
console.log('Invalid kernel dimension');
}
let pix, i, r, g, b;
const w = width;
const h = height;
const cw = w + pad * 2; // add padding
const ch = h + pad * 2;
for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
r = 0;
g = 0;
b = 0;
for (let kx = -pad; kx <= pad; kx++) {
for (let ky = -pad; ky <= pad; ky++) {
i = (ky + pad) * dim + (kx + pad); // kernel index
pix = 4 * ((row + ky) * cw + (col + kx)); // image index
r += pixels[pix++] * kernel[i];
g += pixels[pix++] * kernel[i];
b += pixels[pix ] * kernel[i];
}
}
pix = 4 * ((row - pad) * w + (col - pad)); // destination index
pixels[pix++] = r;
pixels[pix++] = g;
pixels[pix++] = b;
pixels[pix ] = 255; // we want opaque image
}
}
console.log(pixels);
return this.imgData;
}
}

Draw text pixel by pixel on canvas

I have a canvas where I use "fillText" with a string, saying for example "stackoverflow". Then I read the imagedata of the canvas in order to pick out each pixel of that text.
I want to pick the following from the pixel: x position, y position and its color. Then I would like to loop over that array with those pixels so I can draw back the text pixel by pixel so I have full control of each pixel, and can for example animate them.
However, I dont get it as smooth as I want. Look at my attach image, and you see the difference between the top text and then the text I've plotted out using fillRect for each pixel. Any help on how to make the new text look like the "fillText" text does?
Thanks
UPDATE: Added my code
var _particles = [];
var _canvas, _ctx, _width, _height;
(function(){
init();
})();
function init(){
setupParticles(getTextCanvasData());
}
function getTextCanvasData(){
// var w = 300, h = 150, ratio = 2;
_canvas = document.getElementById("textCanvas");
// _canvas.width = w * ratio;
// _canvas.height = h * ratio;
// _canvas.style.width = w + "px";
// _canvas.style.height = h + "px";
_ctx = _canvas.getContext("2d");
_ctx.fillStyle = "rgb(0, 154, 253)";
// _ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
var str = "stackoverflow";
_ctx.font = "32px EB Garamond";
_ctx.fillText(str,0,23);
_width = _canvas.width;
_height = _canvas.height;
var data32 = new Uint32Array(_ctx.getImageData(0, 0, _width, _height).data.buffer);
var positions = [];
for(i = 0; i < data32.length; i++) {
if (data32[i] & 0xffff0000) {
positions.push({
x: (i % _width),
y: ((i / _width)|0),
});
}
}
return positions;
}
function setupParticles(positions){
var i = positions.length;
var particles = [];
while(i--){
var p = new Particle();
p.init(positions[i]);
_particles.push(p);
drawParticle(p);
}
}
function drawParticle(particle){
var x = particle.x;
var y = particle.y;
_ctx.beginPath();
_ctx.fillRect(x, y, 1, 1);
_ctx.fillStyle = 'green';
}
function Particle(){
this.init = function(pos){
this.x = pos.x;
this.y = pos.y + 30;
this.x0 = this.x;
this.y0 = this.y;
this.xDelta = 0;
this.yDelta = 0;
}
}
Here is an update to your code that reuses the alpha component of each pixel. There will still be some detail lost because we do not keep the antialiasing of the pixels (which in effect alters the actual color printed), but for this example the alpha is enough.
var _particles = [];
var _canvas, _ctx, _width, _height;
(function(){
init();
})();
function init(){
setupParticles(getTextCanvasData());
}
function getTextCanvasData(){
// var w = 300, h = 150, ratio = 2;
_canvas = document.getElementById("textCanvas");
// _canvas.width = w * ratio;
// _canvas.height = h * ratio;
// _canvas.style.width = w + "px";
// _canvas.style.height = h + "px";
_ctx = _canvas.getContext("2d");
_ctx.imageSmoothingEnabled= false;
_ctx.fillStyle = "rgb(0, 154, 253)";
// _ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
var str = "stackoverflow";
_ctx.font = "32px EB Garamond";
_ctx.fillText(str,0,23);
_width = _canvas.width;
_height = _canvas.height;
var pixels = _ctx.getImageData(0, 0, _width, _height).data;
var data32 = new Uint32Array(pixels.buffer);
var positions = [];
for(i = 0; i < data32.length; i++) {
if (data32[i] & 0xffff0000) {
positions.push({
x: (i % _width),
y: ((i / _width)|0),
a: pixels[i*4 + 3] / 255
});
}
}
return positions;
}
function setupParticles(positions){
var i = positions.length;
var particles = [];
while(i--){
var p = new Particle();
p.init(positions[i]);
_particles.push(p);
drawParticle(p);
}
}
function drawParticle(particle){
var x = particle.x;
var y = particle.y;
_ctx.beginPath();
_ctx.fillStyle = `rgba(0,128,0,${particle.alpha})`;
_ctx.fillRect(x, y, 1, 1);
}
function Particle(){
this.init = function(pos){
this.x = pos.x;
this.y = pos.y + 30;
this.x0 = this.x;
this.y0 = this.y;
this.xDelta = 0;
this.yDelta = 0;
this.alpha = pos.a;
}
}
<canvas id="textCanvas"></canvas>

How to zoom canvas with animation

I am designing a paint bucket app. My code is working fine. Just need a little help. On zoom in and zoom out buttons, I am changing the height and width of canvas. How can I apply animation to it? This is my complete code. The zoom part is at the end. You can simply copy paste it inside a file. It will work.
<!DOCTYPE html>
<html>
<head>
<title>Painitng</title>
<style>
body {
width: 100%;
height: auto;
text-align: center;
}
.colorpick {
widh: 100%;
height: atuo;
}
.pick {
display: inline-block;
width: 30px;
height: 30px;
margin: 5px;
cursor: pointer;
}
canvas {
border: 2px solid silver;
}
</style>
</head>
<body>
<button id="zoomin">Zoom In</button>
<button id="zoomout">Zoom Out</button>
<button id = "undo-button" onclick="history('undo')">Undo</button>
<button id = "redo-button" onclick="history('redo')">Redo</button>
<div id="canvasDiv"></div>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js"></script>
<script type="text/javascript">
var colorYellow = {
r: 255,
g: 207,
b: 51
};
var context;
var canvasWidth = 500;
var canvasHeight = 500;
var myColor = colorYellow;
var curColor = myColor;
var outlineImage = new Image();
var backgroundImage = new Image();
var drawingAreaX = 0;
var drawingAreaY = 0;
var drawingAreaWidth = 500;
var drawingAreaHeight = 500;
var colorLayerData;
var outlineLayerData;
var totalLoadResources = 2;
var curLoadResNum = 0;
var undoarr = new Array();
var redoarr = new Array();
function history(command){ // handles undo/redo button events.
var data;
if(command === "redo"){
data = historyManager.redo(); // get data for redo
}else
if(command === "undo"){
data = historyManager.undo(); // get data for undo
}
if(data !== undefined){ // if data has been found
setColorLayer(data); // set the data
}
}
// sets colour layer and creates copy into colorLayerData
function setColorLayer(data){
context.putImageData(data, 0, 0);
colorLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight);
context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight);
}
// Clears the canvas.
function clearCanvas() {
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
}
// Draw the elements on the canvas
function redraw() {
uc = 0;
rc = 0;
var locX,
locY;
// Make sure required resources are loaded before redrawing
if (curLoadResNum < totalLoadResources) {
return; // To check if images are loaded successfully or not.
}
clearCanvas();
// Draw the current state of the color layer to the canvas
context.putImageData(colorLayerData, 0, 0);
historyManager.push(context.getImageData(0, 0, canvasWidth, canvasHeight));
redoarr = new Array();
// Draw the background
context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight);
// Draw the outline image on top of everything. We could move this to a separate
// canvas so we did not have to redraw this everyime.
context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight);
}
;
function matchOutlineColor(r, g, b, a) {
return (r + g + b < 100 && a === 255);
}
;
function matchStartColor(pixelPos, startR, startG, startB) {
var r = outlineLayerData.data[pixelPos],
g = outlineLayerData.data[pixelPos + 1],
b = outlineLayerData.data[pixelPos + 2],
a = outlineLayerData.data[pixelPos + 3];
// If current pixel of the outline image is black
if (matchOutlineColor(r, g, b, a)) {
return false;
}
r = colorLayerData.data[pixelPos];
g = colorLayerData.data[pixelPos + 1];
b = colorLayerData.data[pixelPos + 2];
// If the current pixel matches the clicked color
if (r === startR && g === startG && b === startB) {
return true;
}
// If current pixel matches the new color
if (r === curColor.r && g === curColor.g && b === curColor.b) {
return false;
}
return true;
}
;
function colorPixel(pixelPos, r, g, b, a) {
colorLayerData.data[pixelPos] = r;
colorLayerData.data[pixelPos + 1] = g;
colorLayerData.data[pixelPos + 2] = b;
colorLayerData.data[pixelPos + 3] = a !== undefined ? a : 255;
}
;
function floodFill(startX, startY, startR, startG, startB) {
var newPos,
x,
y,
pixelPos,
reachLeft,
reachRight,
drawingBoundLeft = drawingAreaX,
drawingBoundTop = drawingAreaY,
drawingBoundRight = drawingAreaX + drawingAreaWidth - 1,
drawingBoundBottom = drawingAreaY + drawingAreaHeight - 1,
pixelStack = [[startX, startY]];
while (pixelStack.length) {
newPos = pixelStack.pop();
x = newPos[0];
y = newPos[1];
// Get current pixel position
pixelPos = (y * canvasWidth + x) * 4;
// Go up as long as the color matches and are inside the canvas
while (y >= drawingBoundTop && matchStartColor(pixelPos, startR, startG, startB)) {
y -= 1;
pixelPos -= canvasWidth * 4;
}
pixelPos += canvasWidth * 4;
y += 1;
reachLeft = false;
reachRight = false;
// Go down as long as the color matches and in inside the canvas
while (y <= drawingBoundBottom && matchStartColor(pixelPos, startR, startG, startB)) {
y += 1;
colorPixel(pixelPos, curColor.r, curColor.g, curColor.b);
if (x > drawingBoundLeft) {
if (matchStartColor(pixelPos - 4, startR, startG, startB)) {
if (!reachLeft) {
// Add pixel to stack
pixelStack.push([x - 1, y]);
reachLeft = true;
}
} else if (reachLeft) {
reachLeft = false;
}
}
if (x < drawingBoundRight) {
if (matchStartColor(pixelPos + 4, startR, startG, startB)) {
if (!reachRight) {
// Add pixel to stack
pixelStack.push([x + 1, y]);
reachRight = true;
}
} else if (reachRight) {
reachRight = false;
}
}
pixelPos += canvasWidth * 4;
}
}
}
;
// Start painting with paint bucket tool starting from pixel specified by startX and startY
function paintAt(startX, startY) {
var pixelPos = (startY * canvasWidth + startX) * 4,
r = colorLayerData.data[pixelPos],
g = colorLayerData.data[pixelPos + 1],
b = colorLayerData.data[pixelPos + 2],
a = colorLayerData.data[pixelPos + 3];
if (r === curColor.r && g === curColor.g && b === curColor.b) {
// Return because trying to fill with the same color
return;
}
if (matchOutlineColor(r, g, b, a)) {
// Return because clicked outline
return;
}
floodFill(startX, startY, r, g, b);
redraw();
}
;
// Add mouse event listeners to the canvas
function createMouseEvents() {
$('#canvas').mousedown(function (e) {
// Mouse down location
var mouseX = e.pageX - this.offsetLeft,
mouseY = e.pageY - this.offsetTop;
// assuming that the mouseX and mouseY are the mouse coords.
if(this.style.width){ // make sure there is a width in the style
// (assumes if width is there then height will be too
var w = Number(this.style.width.replace("px","")); // warning this will not work if size is not in pixels
var h = Number(this.style.height.replace("px","")); // convert the height to a number
var pixelW = this.width; // get the canvas resolution
var pixelH = this.height;
mouseX = Math.floor((mouseX / w) * pixelW); // convert the mouse coords to pixel coords
mouseY = Math.floor((mouseY / h) * pixelH);
}
if ((mouseY > drawingAreaY && mouseY < drawingAreaY + drawingAreaHeight) && (mouseX <= drawingAreaX + drawingAreaWidth)) {
paintAt(mouseX, mouseY);
}
});
}
;
resourceLoaded = function () {
curLoadResNum += 1;
//if (curLoadResNum === totalLoadResources) {
createMouseEvents();
redraw();
//}
};
var historyManager = (function (){ // Anon for private (closure) scope
var uBuffer = []; // this is undo buff
var rBuffer = []; // this is redo buff
var currentState = undefined; // this holds the current history state
var undoElement = undefined;
var redoElement = undefined;
var manager = {
UI : { // UI interface just for disable and enabling redo undo buttons
assignUndoButton : function(element){
undoElement = element;
this.update();
},
assignRedoButton : function(element){
redoElement = element;
this.update();
},
update : function(){
if(redoElement !== undefined){
redoElement.disabled = (rBuffer.length === 0);
}
if(undoElement !== undefined){
undoElement.disabled = (uBuffer.length === 0);
}
}
},
reset : function(){
uBuffer.length = 0;
rBuffer.length = 0;
currentState = undefined;
this.UI.update();
},
push : function(data){
if(currentState !== undefined){
var same=true
for(i=0;i<data.data.length;i++){
if(data.data[i] !== currentState.data[i]){
same = false;break;
}
} if(same){
return;
}
}
if(currentState !== undefined){
uBuffer.push(currentState);
}
currentState = data;
rBuffer.length = 0;
this.UI.update();
},
undo : function(){
if(uBuffer.length > 0){
if(currentState !== undefined){
rBuffer.push(currentState);
}
currentState = uBuffer.pop();
}
this.UI.update();
return currentState; // return data or unfefined
},
redo : function(){
if(rBuffer.length > 0){
if(currentState !== undefined){
uBuffer.push(currentState);
}
currentState = rBuffer.pop();
}
this.UI.update();
return currentState;
},
}
return manager;
})();
function start() {
var canvas = document.createElement('canvas');
canvas.setAttribute('width', canvasWidth);
canvas.setAttribute('height', canvasHeight);
canvas.setAttribute('id', 'canvas');
document.getElementById('canvasDiv').appendChild(canvas);
if (typeof G_vmlCanvasManager !== "undefined") {
canvas = G_vmlCanvasManager.initElement(canvas);
}
context = canvas.getContext("2d");
backgroundImage.onload = resourceLoaded();
backgroundImage.src = "images/t1.png";
outlineImage.onload = function () {
context.drawImage(outlineImage, drawingAreaX, drawingAreaY, drawingAreaWidth, drawingAreaHeight);
try {
outlineLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
} catch (ex) {
window.alert("Application cannot be run locally. Please run on a server.");
return;
}
clearCanvas();
colorLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
resourceLoaded();
};
outlineImage.src = "images/d.png";
}
;
if(historyManager !== undefined){
// only for visual feedback and not required for the history manager to function.
historyManager.UI.assignUndoButton(document.querySelector("#undo-button"));
historyManager.UI.assignRedoButton(document.querySelector("#redo-button"));
}
getColor = function () {
};
</script>
<script type="text/javascript"> $(document).ready(function () {
start();
});</script>
<script language="javascript">
$('#zoomin').click(function () {
if ($("#canvas").width()==500){
$("#canvas").width(750);
$("#canvas").height(750);
var ctx = canvas.getContext("2d");
ctx.drawImage(backgroundImage, 0, 0, 749, 749);
ctx.drawImage(outlineImage, 0, 0, 749, 749);
redraw();
} else if ($("#canvas").width()==750){
$("#canvas").width(1000);
$("#canvas").height(1000);
var ctx = canvas.getContext("2d");
ctx.drawImage(backgroundImage, 0, 0, 999, 999);
ctx.drawImage(outlineImage, 0, 0, 999, 999);
redraw();
}
});
$('#zoomout').click(function () {
if ($("#canvas").width() == 1000) {
$("#canvas").width(750);
$("#canvas").height(750);
var ctx = canvas.getContext("2d");
ctx.drawImage(backgroundImage, 0, 0, 749, 749);
ctx.drawImage(outlineImage, 0, 0, 749, 749);
redraw();
} else if ($("#canvas").width() == 750) {
$("#canvas").width(500);
$("#canvas").height(500);
var ctx = canvas.getContext("2d");
ctx.drawImage(backgroundImage, 0, 0, 499, 499);
ctx.drawImage(outlineImage, 0, 0, 499, 499);
redraw();
}
});
</script>
<div class="colorpick">
<div class="pick" style="background-color:rgb(150, 0, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 0, 152);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 151, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 0, 5);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 255, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 255, 255);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 0, 255);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 150, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 0, 150);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 255, 150);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(150, 0, 255);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 150, 255);" onclick="hello(this.style.backgroundColor);"></div>
</div>
<script>
function hello(e) {
var rgb = e.replace(/^(rgb|rgba)\(/, '').replace(/\)$/, '').replace(/\s/g, '').split(',');
myColor.r = parseInt(rgb[0]);
myColor.g = parseInt(rgb[1]);
myColor.b = parseInt(rgb[2]);
curColor = myColor;
}
</script>
</body>
</html>
You could continuously redraw the canvas content as you're resizing, but there is a more efficient way to do it.
Alternatively...
Continuously redrawing the canvas content is resource intensive. A more efficient way to handle the zoom-animation is to use CSS to do it. When the animation completes, only then resize the canvas and redraw the content at its final zoomed scale. It's true that while animating the transition the canvas content may temporarily appear stretched or squished, but it all happens so quickly that it will not be noticeable.
Save the unzoomed canvas to another in-memory canvas: var memCanvas = canvas.cloneNode() & drawImage the visible canvas to the memCanvas.
Use CSS animations to resize the canvas element to a zoom size. Here's an example on a previous SO Q&A. The animation completes so quickly it probably won't be noticeable if the content resizes disproportionally.
When the animation is complete (the canvas is at its full-zoomed size) you can scale the saved drawing to the new size with drawImage( savedCanvas, 0,0, originalWidth*zoomFactor, originalHeight*zoomFactor) and reset the CSS width and height to the original size.

Multiple animation in canvas

I want to animate 2 figures on my page. Therefore I use Javascript. The problem is that I can't define which "sprite" he has to take. Because I can't add some Parameters like the object "animation" to the gameLoop()-method, because of the requestAnimationFrame. Any idea how I can solve this?
var animation,
image,
canvas,
stopAtLastFrame = false,
amountAnimation = 0,
amountAnimated = 1;
image = new Image();
function sprite (options) {
var that = {},
frameIndex = 0,
currentRow = 0,
currentColumn = 0,
tickCount = 0,
rowOfLastFrame = 0,
columnOfLastFrame = 0,
ticksPerFrame = options.ticksPerFrame,
numberOfFrames = options.numberOfFrames,
numberOfColumns = options.totalColumns,
numberOfRows = options.totalRows,
widthOfOneFrame = options.width/numberOfColumns,
heightOfOneFrame = options.height/numberOfRows;
that.context = options.context;
that.width = options.width;
that.height = options.height;
that.image = options.image;
rowOfLastFrame = numberOfRows-1;
columnOfLastFrame = numberOfColumns - 1 - ((numberOfRows *numberOfColumns) - numberOfFrames);
that.render = function () {
that.context.save();
that.context.scale(0.5,0.5);
// Clear the canvas
that.context.clearRect(0, 0, that.width, that.height);
// Draw the animation
that.context.drawImage(
that.image,
currentColumn * widthOfOneFrame,
currentRow * heightOfOneFrame,
widthOfOneFrame,
heightOfOneFrame,
0,
0,
widthOfOneFrame,
heightOfOneFrame);
that.context.restore();
if(rowOfLastFrame==currentRow && columnOfLastFrame==currentColumn && amountAnimated==amountAnimation)
{
stopAtLastFrame=true;
}
};
that.update = function () {
tickCount += 1;
if (tickCount > ticksPerFrame) {
tickCount = 0;
if (frameIndex < numberOfFrames-1) {
frameIndex++;
currentColumn++;
if (currentColumn == numberOfColumns) {
currentRow++;
currentColumn = 0;
}
}else{
amountAnimated++;
frameIndex=0;
currentColumn=0;
currentRow=0;
}
}
};
return that;
}
function gameLoop () {
if(stopAtLastFrame==true)
{
stopAtLastFrame=false;
}
else
{
window.requestAnimationFrame(gameLoop);
animation.update();
animation.render();
}
}
function startAnimation(canvas, imageUrl, amount){
canvas = document.getElementById(canvas);
canvas.width = 7680/4;
canvas.height = 2880/4;
image.src = imageUrl;
amountAnimation=amount;
animation = sprite({
context: canvas.getContext("2d"),
width: 7684,
height: 2880,
image: image,
numberOfFrames:16,
totalRows : 4,
totalColumns: 4,
ticksPerFrame: 5
});
gameLoop();
}
startAnimation("imageAnimation", "img/16.png", 3);
startAnimation("imageAnimation2", "img/32.png", 5);
In this example I pretend that my spritesheets are the same size and columns, just to test the code.
This doesn't work of course, but this is what I need:
function gameLoop (animation) {
if(stopAtLastFrame==true)
{
stopAtLastFrame=false;
}
else
{
window.requestAnimationFrame(gameLoop(animation));
animation.update();
animation.render();
}
}
requestAnimationFrame function is not allowed to pass your own parameter, one solution/trick is bound that gameLoop function to sprite object, so that we can access this as sprite object.
as example: http://jsfiddle.net/XQpzU/6110/
Your code can be modified as:
var animation,
image,
canvas,
stopAtLastFrame = false,
amountAnimation = 0,
amountAnimated = 1;
image = new Image();
function sprite (options) {
var that = {},
frameIndex = 0,
currentRow = 0,
currentColumn = 0,
tickCount = 0,
rowOfLastFrame = 0,
columnOfLastFrame = 0,
ticksPerFrame = options.ticksPerFrame,
numberOfFrames = options.numberOfFrames,
numberOfColumns = options.totalColumns,
numberOfRows = options.totalRows,
widthOfOneFrame = options.width/numberOfColumns,
heightOfOneFrame = options.height/numberOfRows;
that.context = options.context;
that.width = options.width;
that.height = options.height;
that.image = options.image;
rowOfLastFrame = numberOfRows-1;
columnOfLastFrame = numberOfColumns - 1 - ((numberOfRows *numberOfColumns) - numberOfFrames);
that.render = function () {
that.context.save();
that.context.scale(0.5,0.5);
// Clear the canvas
that.context.clearRect(0, 0, that.width, that.height);
// Draw the animation
that.context.drawImage(
that.image,
currentColumn * widthOfOneFrame,
currentRow * heightOfOneFrame,
widthOfOneFrame,
heightOfOneFrame,
0,
0,
widthOfOneFrame,
heightOfOneFrame);
that.context.restore();
if(rowOfLastFrame==currentRow && columnOfLastFrame==currentColumn && amountAnimated==amountAnimation)
{
stopAtLastFrame=true;
}
};
that.update = function () {
tickCount += 1;
if (tickCount > ticksPerFrame) {
tickCount = 0;
if (frameIndex < numberOfFrames-1) {
frameIndex++;
currentColumn++;
if (currentColumn == numberOfColumns) {
currentRow++;
currentColumn = 0;
}
}else{
amountAnimated++;
frameIndex=0;
currentColumn=0;
currentRow=0;
}
}
};
that.gameLoop = gameLoop.bind(that);
return that;
}
function gameLoop () {
if(stopAtLastFrame==true)
{
stopAtLastFrame=false;
}
else
{
// this will be your sprite
window.requestAnimationFrame(this.gameLoop);
animation.update();
animation.render();
}
}
function startAnimation(canvas, imageUrl, amount){
canvas = document.getElementById(canvas);
canvas.width = 7680/4;
canvas.height = 2880/4;
image.src = imageUrl;
amountAnimation=amount;
animation = sprite({
context: canvas.getContext("2d"),
width: 7684,
height: 2880,
image: image,
numberOfFrames:16,
totalRows : 4,
totalColumns: 4,
ticksPerFrame: 5
});
animation.gameLoop();
}
startAnimation("imageAnimation", "img/16.png", 3);
startAnimation("imageAnimation2", "img/32.png", 5);

Categories