Animating with the `ImageData` data array causes very unexpected and glitch-like results - html5-canvas

I recently stretched a gradient across the canvas using the ImageData data array; ie the ctx.getImageData() and ctx.putImageData() methods, and thought to myself, "this could be a really efficient way to animate a canvas full of moving objects". So I wrapped it into my main function along with the requestAnimationFrame(callback) statement, but that's when things got weird. The best I can do at describing is to say it's like the left most column of pixels on the canvas is duplicated in the right most column of pixels, and based on what coordinates you specify for the get and put ctx methods, this can have bizarre consequences.
I started out with the get and put methods targeting the canvas at 0, 0 like so:
imageData = ctx.getImageData( 0, 0, cvs.width, cvs.height );
// here I set the pixel colors according to their location
// on the canvas using nested for loops to target the
// the correct imageData array indexes.
ctx.putImageData( imageData, 0, 0 );
But I immediately noticed the right side of the canvas was wrong. Like the gradient has started over, and the last pixel just didn't get touched for some reason:
So scaled back my draw region changed the put ImageData coordinates to get some space between the drawn image and the edge of the canvas, and I changed the get coordinated to eliminate that line on the right edge of the canvas:
imageData = ctx.getImageData( 1, 1, cvs.width, cvs.height );
for ( var x = 0; x < cvs.width - 92; x++ ) {
for ( var y = 0; y < cvs.height - 92; y++ ) {
// array[ x + y * width ] = value / x; // or similar
}
}
ctx.putImageData( imageData, 2, 2 );
Pretty! But wrong... So I reproduced it in codepen. Can someone help me understand and overcome this behavior?
Note: The codepen has the scaled back draw area. If you change the get coordinates to 0 you'll see it basically behaves the same way as the first example but with white-space in between the expected square and the unexpected line. That said, I left the get at 1 and the put at zero for the most interesting behavior yet.

I've changed your code a little. In your double loop I am declaring a variable var i = (x + y*cvs.width)*4; This is only reducing the verbosity of your code so that I can see it better. The i variable represents the index of your pixel in the imageData.data array. Since you are doing
imageData.data[i - 4 ] ...
imageData.data[i - 3 ] ...
imageData.data[i - 2 ] ...
imageData.data[i - 1 ] ...
you are going one pixel backwards and the first pixel from every row appears as the last pixel of the previous row. So I've changed it from var i = (x + y*cvs.width)*4; to var i = 4 + (x + y*cvs.width)*4;.
When you are animating it, since the imageData is inside the test() function, you are recalculating the values of the imageData.data array in base of the last frame. So in the second frame you have that 1px line from the first frame copied again and moved 1px upward and 1px to the left.
I hope this is what you were asking.
var ctx, cvs, imageData;
cvs = document.getElementById('canv');
ctx = cvs.getContext('2d');
function test() {
// imageData = ctx.getImageData( 0, 0, cvs.width, cvs.height );
// produces a line on the right side of the screen
imageData = ctx.getImageData( 1, 1, cvs.width, cvs.height );
// bizzar reverse cascading
for (var x=0;x<cvs.width-92;x++) {
for (var y=0;y<cvs.height-92;y++) {
var i = 4+(x + y*cvs.width)*4;
imageData.data[i - 4 ] = Math.floor((255-y)-Math.floor(x/55)*55);
imageData.data[i - 3 ] = Math.floor(255/(cvs.height-92)*y);
imageData.data[i - 2 ] = Math.floor(255/(cvs.width-92)*x);
imageData.data[i - 1 ] = 255;
}
}
ctx.putImageData( imageData, 0, 0 );
requestAnimationFrame( test );
}
test();
canvas {
box-shadow: 0 0 2.5px 0 black;
}
<canvas id="canv" height="256" width="256"></canvas>

Related

ThreeJS - THREE.BufferGeometry.computeBoundingSphere() Gives Error: NaN Position Values

I am creating a simple THREE.PlaneBufferGeometry using Threejs. The surface is a geologic surface in the earth.
This surface has local gaps or 'holes' in it represented by NaN's. I have read another similar, but older, post where the suggestion was to fill the position Z component with 'undefined' rather than NaN. I tried that but get this error:
THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values.
PlaneBufferGeometry {uuid: "8D8EFFBF-7F10-4ED5-956D-5AE1EAD4DD41", name: "", type: "PlaneBufferGeometry", index: Uint16BufferAttribute, attributes: Object, …}
Here is the TypeScript function that builds the surface:
AddSurfaces(result) {
let surfaces: Surface[] = result;
if (this.surfaceGroup == null) {
this.surfaceGroup = new THREE.Group();
this.globalGroup.add(this.surfaceGroup);
}
surfaces.forEach(surface => {
var material = new THREE.MeshPhongMaterial({ color: 'blue', side: THREE.DoubleSide });
let mesh: Mesh2D = surface.arealMesh;
let values: number[][] = surface.values;
let geometry: PlaneBufferGeometry = new THREE.PlaneBufferGeometry(mesh.width, mesh.height, mesh.nx - 1, mesh.ny - 1);
var positions = geometry.getAttribute('position');
let node: number = 0;
// Surfaces in Three JS are ordered from top left corner x going fastest left to right
// and then Y ('j') going from top to bottom. This is backwards in Y from how we do the
// modelling in the backend.
for (let j = mesh.ny - 1; j >= 0; j--) {
for (let i = 0; i < mesh.nx; i++) {
let value: number = values[i][j];
if(!isNaN(values[i][j])) {
positions.setZ(node, -values[i][j]);
}
else {
positions.setZ(node, undefined); /// This does not work? Any ideas?
}
node++;
}
}
geometry.computeVertexNormals();
var plane = new THREE.Mesh(geometry, material);
plane.receiveShadow = true;
plane.castShadow = true;
let xOrigin: number = mesh.xOrigin;
let yOrigin: number = mesh.yOrigin;
let cx: number = xOrigin + (mesh.width / 2.0);
let cy: number = yOrigin + (mesh.height / 2.0);
// translate point to origin
let tempX: number = xOrigin - cx;
let tempY: number = yOrigin - cy;
let azi: number = mesh.azimuth;
let aziRad = azi * Math.PI / 180.0;
// now apply rotation
let rotatedX: number = tempX * Math.cos(aziRad) - tempY * Math.sin(aziRad);
let rotatedY: number = tempX * Math.sin(aziRad) + tempY * Math.cos(aziRad);
cx += (tempX - rotatedX);
cy += (tempY - rotatedY);
plane.position.set(cx, cy, 0.0);
plane.rotateZ(aziRad);
this.surfaceGroup.add(plane);
});
this.UpdateCamera();
this.animate();
}
Thanks!
I have read another similar, but older, post where the suggestion was to fill the position Z component with 'undefined' rather than NaN.
Using undefined will fail in the same way like using NaN. BufferGeometry.computeBoundingSphere() computes the radius based on Vector3.distanceToSquared(). If you call this method with a vector that contains no valid numerical data, NaN will be returned.
Hence, you can't represent the gaps in a geometry with NaN or undefined position data. The better way is to generate a geometry which actually represents the geometry of your geologic surface. Using ShapeBufferGeometry might be a better candidate since shapes do support the concept of holes.
three.js r117
THREE.PlaneBufferGeometry:: parameters: {
width: number;
height: number;
widthSegments: number;
heightSegments: number;
};
widthSegments or heightSegments should be greater 1 ,if widthSegments < 1 ,widthSegments may be equal 0 or nan.
In my case, it was happening when I tried to create a beveled shape based on a single vector or a bunch of identical vectors - so there was only a single point. Filtering out such shapes solved the issue.

THREEJS - Indexed BufferGeometry with 2 materials

I want to create ONE single buffer geometry that can hold many materials.
I have read that in order to achieve this in BufferGeometry, I need to use groups. So I created the following "floor" mesh:
var gg=new THREE.BufferGeometry(),vtx=[],fc=[[],[]],mm=[
new THREE.MeshLambertMaterial({ color:0xff0000 }),
new THREE.MeshLambertMaterial({ color:0x0000ff })
];
for(var y=0 ; y<11 ; y++)
for(var x=0 ; x<11 ; x++) {
vtx.push(x-5,0,y-5);
if(x&&y) {
var p=(vtx.length/3)-1;
fc[(x%2)^(y%2)].push(
p,p-11,p-1,
p-1,p-11,p-12
);
}
}
gg.addAttribute('position',new THREE.Float32BufferAttribute(vtx,3));
Array.prototype.push.apply(fc[0],fc[1]); gg.setIndex(fc[0]);
gg.computeVertexNormals();
gg.addGroup(0,100,0);
gg.addGroup(100,100,1);
scene.add(new THREE.Mesh(gg,mm));
THE ISSUE:
looking at the example in https://www.crazygao.com/vc/tst2.htm can see that the BLUE material looks weird.
Single material showup OK.
2 materials with group as above, in any case show the BLUE really strage.
Changing the 1st group to start=0, count=200 (for all triangles) and removing the 2nd group, will show MORE squares of RED (obviously) but still NOT in the way I would like it to show.
Changing the 1st group count to any value greater than 200 will cause a crash (obviously) of attempting to access vertex out of range...
Is anyone know clearly what shall I do?
I am using THREE.js v.101 and I prefer to not create special custom shader for that, or add another vertex buffer to duplicate those I already have, and I prefer to not create 2 meshes as this may get much more complicated with advanced models.
Check out this: https://jsfiddle.net/mmalex/zebos3va/
fix #1 - don't define group 0
fix #2 - 2nd parameter in .addGroup is buffer length, it must be multiple of 3 (100 was wrong)
var gg = new THREE.BufferGeometry(),
vtx = [],
fc = [[],[]],
mm = [
new THREE.MeshLambertMaterial({
color: 0xff0000
}),
new THREE.MeshLambertMaterial({
color: 0x0000ff
})
];
for (var y = 0; y < 11; y++)
for (var x = 0; x < 11; x++) {
vtx.push(x - 5, 0, y - 5);
if (x && y) {
var p = (vtx.length / 3) - 1;
fc[(x % 2) ^ (y % 2)].push(
p, p - 11, p - 1,
p - 1, p - 11, p - 12
);
}
}
gg.addAttribute('position', new THREE.Float32BufferAttribute(vtx, 3));
fc[0].push.apply(fc[1]);
gg.setIndex(fc[0]);
gg.computeVertexNormals();
// group 0 is everything, unless you define group 1
// fix #1 - don't define group 0
// fix #2 - 2nd parameter is buffer length, it must be multiple of 3 (100 was wrong)
gg.addGroup(0, 102, 1);
scene.add(new THREE.Mesh(gg, mm));

Can a skybox be a single image, and if not why?

When I search for some skybox images (e.g. google images) I am getting hits showing single images in a sideways cross pattern. But all the three.js examples (for example) I've managed to find show loading 6 images.
It feels strange that I have to cut up a single image, and then have the extra load of 6 images instead of one image.
The documentation is a bit vague (i.e. as to whether 6 images is an option, or the only way to do it).
Here is a question that seems to be using a single image, but it is one row, and the answer uses a 2x3 grid; neither of them are the cross shape!
(BTW, bonus question: I tried working this out from the source code, but where is it? The THREE.CubeTextureLoader().load() code is a loop to load however many URLs it is given (NB. no checking that it is 6), then calls THREE.Texture, which seems very generic.)
Answer: yes, it can definitely be a single image. You have the proof in the stackoverflow question you provided.
Why the cross images exists: Sometimes (I have done this in OpenGl), you can specify coordinates in you images, for each one of the 6 faces of your cube. It doesn't look like the three.js library offers this functionnality.
What they offer is the .repeat and .offset attributes. This is what is being used in the single image jsfiddle.
for ( var i = 0; i < 6; i ++ ) {
t[i] = THREE.ImageUtils.loadTexture( imgData ); //2048x256
t[i].repeat.x = 1 / 8;
t[i].offset.x = i / 8;
// Could also use repeat.y and offset.y, which are probably used in the 2x3 image
....
You can experiment with the fiddle to see what happens if you modify those values. i.e.
t[i].repeat.x = 1 / 4;
t[i].offset.x = i / 4;
Good luck, hope this helped.
Bonus question edit : Also, in the THREE.CubeTextureLoader().load() code, it does in fact do an automatic update once 6 images have been loaded :
...
if ( loaded === 6 ) {
texture.needsUpdate = true;
if ( onLoad ) onLoad( texture );
}
FYI, correct mapping after tried it out:
x_offset = [3, 1, 2, 2, 2, 0];
y_offset = [1, 1, 2, 0, 1, 1];
for ( var i = 0; i < 6; i ++ ) {
t[i] = loader.load( imgData, render ); //2330 x 1740 // cubemap
t[i].repeat.x = 1 / 4;
t[i].offset.x = x_offset[i] / 4;
t[i].repeat.y = 1 / 3;
t[i].offset.y = y_offset[i] / 3;
//t[i].magFilter = THREE.NearestFilter;
t[i].minFilter = THREE.NearestFilter;
t[i].generateMipmaps = false;
materials.push( new THREE.MeshBasicMaterial( { map: t[i] } ) );
}

Draw a circular segment progress in SWIFT

I would like to create a circular progress with slices where each slice is an arc.
I based my code on this answer:
Draw segments from a circle or donut
But I don't know how to copy it and rotate it 10 times.
And I would like to color it following a progress variable (in percent).
EDIT: I would like something like this
Any help please
Regards
You could use a circular path and set the strokeStart and StrokeEnd. Something like this:
let circlePath = UIBezierPath(ovalInRect: CGRect(x: 200, y: 200, width: 150, height: 150))
var segments: [CAShapeLayer] = []
let segmentAngle: CGFloat = (360 * 0.125) / 360
for var i = 0; i < 8; i++ {
let circleLayer = CAShapeLayer()
circleLayer.path = circlePath.CGPath
// start angle is number of segments * the segment angle
circleLayer.strokeStart = segmentAngle * CGFloat(i)
// end angle is the start plus one segment, minus a little to make a gap
// you'll have to play with this value to get it to look right at the size you need
let gapSize: CGFloat = 0.008
circleLayer.strokeEnd = circleLayer.strokeStart + segmentAngle - gapSize
circleLayer.lineWidth = 10
circleLayer.strokeColor = UIColor(red:0, green:0.004, blue:0.549, alpha:1).CGColor
circleLayer.fillColor = UIColor.clearColor().CGColor
// add the segment to the segments array and to the view
segments.insert(circleLayer, atIndex: i)
view.layer.addSublayer(segments[i])
}

Random circles being detected

I'm trying to detect circles but I am detecting circles that aren't even there. My code is below. Anyone know how to modify the DetectCircle() method to make the detection more accurate , please and thanks
void detectCircle( IplImage * img )
{
int edge_thresh = 1;
IplImage *gray = cvCreateImage( cvSize(img->width,img->height), 8, 1);
IplImage *edge = cvCreateImage( cvSize(img->width,img->height), 8, 1);
cvCvtColor(img, gray, CV_BGR2GRAY);
gray->origin = 1;
// color threshold
cvThreshold(gray,gray,100,255,CV_THRESH_BINARY);
// smooths out image
cvSmooth(gray, gray, CV_GAUSSIAN, 11, 11);
// get edges
cvCanny(gray, edge, (float)edge_thresh, (float)edge_thresh*3, 5);
// detects circle
CvSeq* circle = cvHoughCircles(edge, cstorage, CV_HOUGH_GRADIENT, 1,
edge->height/50, 5, 35);
// draws circle and its centerpoint
float* p = (float*)cvGetSeqElem( circle, 0 );
if( p==null ){ return;}
cvCircle( img, cvPoint(cvRound(p[0]),cvRound(p[1])), 3, CV_RGB(255,0,0), -1, 8, 0 );
cvCircle( img, cvPoint(cvRound(p[0]),cvRound(p[1])), cvRound(p[2]), CV_RGB(200,0,0), 1, 8, 0 );
cvShowImage ("Snooker", img );
}
cvHoughCircles detects circles that arent obvious to us. If you know the pixel size of snooker balls you can filter them based on their radius. Try setting the min_radius and max_radius parameters in your cvHoughCircles function.
On a side note, once you get the circles, you can filter them based on color. If the circle is mostly one color, it has a good chance of being a ball, if it doenst its probably a false positive.
edit: by "circle's color" i mean the pixels inside the circle boundary

Resources