Distorting images using FabricJS filters and custom controls, by dragging the corner control points of an image - html5-canvas

I am trying to implement distortion in Fabric.js. And it is working preatty good so far. But now I need to make it work when the object has an angle/rotation. And I Cant wrap my head around why it is not working.
I have been following this code. Question
When adding angle to the object the image will move the other corners/controls as well. But when i set the angle to 0 or remove it. It will work. Does any one know how to make it work with the angle/rotation. I want the corners to stay in place.
I tried to add the rotation to the _renderFill function. But did not work.
I also made a new fiddle
fabric.textureSize = 4096;
// Set default filter backend
fabric.filterBackend = new fabric.WebglFilterBackend();
fabric.isWebglSupported(fabric.textureSize);
fabric.Image.filters.Perspective = class extends fabric.Image.filters.BaseFilter {
/**
* Constructor
* #param {Object} [options] Options object
*/
constructor(options) {
super();
if (options) this.setOptions(options);
this.applyPixelRatio();
}
type = 'Perspective';
pixelRatio = fabric.devicePixelRatio;
bounds = {width: 0, height: 0, minX: 0, maxX: 0, minY: 0, maxY: 0};
hasRelativeCoordinates = true;
/**
* Array of attributes to send with buffers. do not modify
* #private
*//** #ts-ignore */
vertexSource = `
precision mediump float;
attribute vec2 aPosition;
attribute vec2 aUvs;
uniform float uStepW;
uniform float uStepH;
varying vec2 vUvs;
vec2 uResolution;
void main() {
vUvs = aUvs;
uResolution = vec2(uStepW, uStepH);
gl_Position = vec4(uResolution * aPosition * 2.0 - 1.0, 0.0, 1.0);
}
`;
fragmentSource = `
precision mediump float;
varying vec2 vUvs;
uniform sampler2D uSampler;
void main() {
gl_FragColor = texture2D(uSampler, vUvs);
}
`;
/**
* Return a map of attribute names to WebGLAttributeLocation objects.
*
* #param {WebGLRenderingContext} gl The canvas context used to compile the shader program.
* #param {WebGLShaderProgram} program The shader program from which to take attribute locations.
* #returns {Object} A map of attribute names to attribute locations.
*/
getAttributeLocations(gl, program) {
return {
aPosition: gl.getAttribLocation(program, 'aPosition'),
aUvs: gl.getAttribLocation(program, 'aUvs'),
};
}
/**
* Send attribute data from this filter to its shader program on the GPU.
*
* #param {WebGLRenderingContext} gl The canvas context used to compile the shader program.
* #param {Object} attributeLocations A map of shader attribute names to their locations.
*/
sendAttributeData(gl, attributeLocations, data, type = 'aPosition') {
const attributeLocation = attributeLocations[type];
if (gl[type + 'vertexBuffer'] == null) {
gl[type + 'vertexBuffer'] = gl.createBuffer();
}
gl.bindBuffer(gl.ARRAY_BUFFER, gl[type+'vertexBuffer']);
gl.enableVertexAttribArray(attributeLocation);
gl.vertexAttribPointer(attributeLocation, 2, gl.FLOAT, false, 0, 0);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
}
generateSurface() {
const corners = this.perspectiveCoords;
const surface = verb.geom.NurbsSurface.byCorners(...corners);
const tess = surface.tessellate();
return tess;
}
/**
* Apply the resize filter to the image
* Determines whether to use WebGL or Canvas2D based on the options.webgl flag.
*
* #param {Object} options
* #param {Number} options.passes The number of filters remaining to be executed
* #param {Boolean} options.webgl Whether to use webgl to render the filter.
* #param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered.
* #param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn.
* #param {WebGLRenderingContext} options.context The GL context used for rendering.
* #param {Object} options.programCache A map of compiled shader programs, keyed by filter type.
*/
applyTo(options) {
if (options.webgl) {
const { width, height } = this.getPerspectiveBounds();
options.context.canvas.width = width;
options.context.canvas.height = height;
options.destinationWidth = width;
options.destinationHeight = height;
this.hasRelativeCoordinates && this.calculateCoordsByCorners();
this._setupFrameBuffer(options);
this.applyToWebGL(options);
this._swapTextures(options);
}
}
applyPixelRatio(coords = this.perspectiveCoords) {
for(let i = 0; i < coords.length; i++) {
coords[i][0] *= this.pixelRatio;
coords[i][1] *= this.pixelRatio;
}
return coords;
}
getPerspectiveBounds(coords = this.perspectiveCoords) {
coords = this.perspectiveCoords.slice().map(c => (
{
x: c[0],
y: c[1],
}
));
this.bounds.minX = fabric.util.array.min(coords, 'x') || 0;
this.bounds.minY = fabric.util.array.min(coords, 'y') || 0;
this.bounds.maxX = fabric.util.array.max(coords, 'x') || 0;
this.bounds.maxY = fabric.util.array.max(coords, 'y') || 0;
this.bounds.width = Math.abs(this.bounds.maxX - this.bounds.minX);
this.bounds.height = Math.abs(this.bounds.maxY - this.bounds.minY);
return {
width: this.bounds.width,
height: this.bounds.height,
minX: this.bounds.minX,
maxX: this.bounds.maxX,
minY: this.bounds.minY,
maxY: this.bounds.maxY,
};
}
/**
* #description coordinates are coming in relative to mockup item sections
* the following function normalizes the coords based on canvas corners
*
* #param {number[]} coords
*/
calculateCoordsByCorners(coords = this.perspectiveCoords) {
for(let i = 0; i < coords.length; i++) {
coords[i][0] -= this.bounds.minX;
coords[i][1] -= this.bounds.minY;
}
}
/**
* Apply this filter using webgl.
*
* #param {Object} options
* #param {Number} options.passes The number of filters remaining to be executed
* #param {Boolean} options.webgl Whether to use webgl to render the filter.
* #param {WebGLTexture} options.originalTexture The texture of the original input image.
* #param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered.
* #param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn.
* #param {WebGLRenderingContext} options.context The GL context used for rendering.
* #param {Object} options.programCache A map of compiled shader programs, keyed by filter type.
*/
applyToWebGL(options) {
const gl = options.context;
const shader = this.retrieveShader(options);
const tess = this.generateSurface(options.sourceWidth, options.sourceHeight);
const indices = new Uint16Array(_.flatten(tess.faces));
// Clear the canvas first
this.clear(gl); // !important
// bind texture buffer
this.bindTexture(gl, options);
gl.useProgram(shader.program);
// create the buffer
this.indexBuffer(gl, indices);
this.sendAttributeData(gl, shader.attributeLocations, new Float32Array(_.flatten(tess.points)), 'aPosition');
this.sendAttributeData(gl, shader.attributeLocations, new Float32Array(_.flatten(tess.uvs)), 'aUvs');
gl.uniform1f(shader.uniformLocations.uStepW, 1 / gl.canvas.width);
gl.uniform1f(shader.uniformLocations.uStepH, 1 / gl.canvas.height);
this.sendUniformData(gl, shader.uniformLocations);
gl.viewport(0, 0, options.destinationWidth, options.destinationHeight);
// enable indices up to 4294967296 for webGL 1.0
gl.getExtension('OES_element_index_uint');
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
}
clear(gl) {
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
bindTexture(gl, options) {
if (options.pass === 0 && options.originalTexture) {
gl.bindTexture(gl.TEXTURE_2D, options.originalTexture);
} else {
gl.bindTexture(gl.TEXTURE_2D, options.sourceTexture);
}
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
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);
}
indexBuffer(gl, data) {
const indexBuffer = gl.createBuffer();
// make this buffer the current 'ELEMENT_ARRAY_BUFFER'
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// Fill the current element array buffer with data
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);
}
};
/**
* Returns filter instance from an object representation
* #static
* #param {Object} object Object to create an instance from
* #param {function} [callback] to be invoked after filter creation
* #return {fabric.Image.filters.Perspective} Instance of fabric.Image.filters.Perspective
*/
fabric.Image.filters.Perspective.fromObject = fabric.Image.filters.BaseFilter.fromObject;
/**
* Photo subclass
* #class fabric.Photo
* #extends fabric.Photo
* #return {fabric.Photo} thisArg
*
*/
fabric.Photo = class extends fabric.Image {
type = 'photo';
repeat = 'no-repeat';
fill = 'transparent';
initPerspective = true;
cacheProperties = fabric.Image.prototype.cacheProperties.concat('perspectiveCoords');
constructor(src, options) {
super(options);
if (options) this.setOptions(options);
this.on('added', () => {
const image = new Image();
image.setAttribute('crossorigin', 'anonymous');
image.onload = () => {
this._initElement(image, options);
this.width = image.width / 2;
this.height = image.height / 2;
this.loaded = true;
this.setCoords();
this.fire('image:loaded');
};
image.src = src;
this.on('image:loaded', () => {
!this.perspectiveCoords && this.getInitialPerspective();
this.togglePerspective();
this.canvas.requestRenderAll();
});
});
}
cacheProperties = fabric.Image.prototype.cacheProperties.concat('perspectiveCoords');
/**
* #private
* #param {CanvasRenderingContext2D} ctx Context to render on
*//** #ts-ignore */
_render(ctx) {
fabric.util.setImageSmoothing(ctx, this.imageSmoothing);
if (this.isMoving !== true && this.resizeFilter && this._needsResize()) {
this.applyResizeFilters();
}
this._stroke(ctx);
this._renderPaintInOrder(ctx);
}
/**
* #private
* #param {CanvasRenderingContext2D} ctx Context to render on
*//** #ts-ignore */
_renderFill(ctx) {
var elementToDraw = this._element;
if (!elementToDraw) return;
ctx.save();
const elWidth = elementToDraw.naturalWidth || elementToDraw.width;
const elHeight = elementToDraw.naturalHeight || elementToDraw.height;
const width = this.width;
const height = this.height;
ctx.translate(-width / 2, -height / 2);
// get the scale
const scale = Math.min(width / elWidth, height / elHeight);
// get the top left position of the image
const x = (width / 2) - (elWidth / 2) * scale;
const y = (height / 2) - (elHeight / 2) * scale;
ctx.drawImage(elementToDraw, x, y, elWidth * scale, elHeight * scale);
ctx.restore();
}
togglePerspective(mode = true) {
this.set('perspectiveMode', mode);
// this.set('hasBorders', !mode);
if (mode === true) {
this.set('layout', 'fit');
var lastControl = this.perspectiveCoords.length - 1;
this.controls = this.perspectiveCoords.reduce((acc, coord, index) => {
const anchorIndex = index > 0 ? index - 1 : lastControl;
let name = `prs${index + 1}`;
acc[name] = new fabric.Control({
name,
x: -0.5,
y: -0.5,
actionHandler: this._actionWrapper(anchorIndex, (_, transform, x, y) => {
const target = transform.target;
const localPoint = target.toLocalPoint(new fabric.Point(x, y), 'left', 'top');
coord[0] = localPoint.x / target.scaleX * fabric.devicePixelRatio;
coord[1] = localPoint.y / target.scaleY * fabric.devicePixelRatio;
target.setCoords();
target.applyFilters();
return true;
}),
positionHandler: function (dim, finalMatrix, fabricObject) {
const zoom = fabricObject.canvas.getZoom();
const scalarX = fabricObject.scaleX * zoom / fabric.devicePixelRatio;
const scalarY = fabricObject.scaleY * zoom / fabric.devicePixelRatio;
var point = fabric.util.transformPoint({
x: this.x * dim.x + this.offsetX + coord[0] * scalarX,
y: this.y * dim.y + this.offsetY + coord[1] * scalarY,
}, finalMatrix
);
return point;
},
cursorStyleHandler: () => 'cell',
render: function(ctx, left, top, _, fabricObject) {
const zoom = fabricObject.canvas.getZoom();
const scalarX = fabricObject.scaleX * zoom / fabric.devicePixelRatio;
const scalarY = fabricObject.scaleY * zoom / fabric.devicePixelRatio;
ctx.save();
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.strokeStyle = 'green';
if (fabricObject.perspectiveCoords[index + 1]) {
ctx.strokeStyle = 'green';
ctx.lineTo(
(fabricObject.perspectiveCoords[index + 1][0] - coord[0]) * scalarX,
(fabricObject.perspectiveCoords[index + 1][1] - coord[1]) * scalarY,
);
} else {
ctx.lineTo(
(fabricObject.perspectiveCoords[0][0] - coord[0]) * scalarX,
(fabricObject.perspectiveCoords[0][1] - coord[1]) * scalarY,
);
}
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, 4, 0, Math.PI * 2);
ctx.closePath();
ctx.fillStyle = 'green';
ctx.fill();
ctx.stroke();
ctx.restore();
},
offsetX: 0,
offsetY: 0,
actionName: 'perspective-coords',
});
return acc;
}, {});
} else {
this.controls = fabric.Photo.prototype.controls;
}
this.canvas.requestRenderAll();
}
_actionWrapper(anchorIndex, fn) {
return function(eventData, transform, x, y) {
if (!transform || !eventData) return;
const { target } = transform;
target._resetSizeAndPosition(anchorIndex);
const actionPerformed = fn(eventData, transform, x, y);
return actionPerformed;
};
}
/**
* #description manually reset the bounding box after points update
*
* #see http://fabricjs.com/custom-controls-polygon
* #param {number} index
*/
_resetSizeAndPosition = (index, apply = true) => {
const absolutePoint = fabric.util.transformPoint({
x: this.perspectiveCoords[index][0],
y: this.perspectiveCoords[index][1],
}, this.calcTransformMatrix());
let { height, width, left, top } = this._calcDimensions({});
const widthDiff = (width - this.width) / 2;
if ((left < 0 && widthDiff > 0) || (left > 0 && widthDiff < 0)) {
absolutePoint.x -= widthDiff;
} else {
absolutePoint.x += widthDiff;
}
const heightDiff = (height - this.height) / 2;
if ((top < 0 && heightDiff > 0) || (top > 0 && heightDiff < 0)) {
absolutePoint.y -= heightDiff;
} else {
absolutePoint.y += heightDiff;
}
this._setPositionDimensions({});
const penBaseSize = this._getNonTransformedDimensions();
const newX = (this.perspectiveCoords[index][0]) / penBaseSize.x;
const newY = (this.perspectiveCoords[index][1]) / penBaseSize.y;
this.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5);
apply && this._applyPointsOffset();
}
/**
* This is modified version of the internal fabric function
* this helps determine the size and the location of the path
*
* #param {object} options
*/
_setPositionDimensions(options) {
const { left, top, width, height } = this._calcDimensions(options);
this.width = width;
this.height = height;
var correctLeftTop = this.translateToGivenOrigin(
{
x: left,
y: top,
},
'left',
'top',
this.originX,
this.originY
);
if (typeof options.left === 'undefined') {
this.left = correctLeftTop.x;
}
if (typeof options.top === 'undefined') {
this.top = correctLeftTop.y;
}
this.pathOffset = {
x: left,
y: top,
};
return { left, top, width, height };
}
/**
* #description this is based on fabric.Path._calcDimensions
*
* #private
*/
_calcDimensions() {
const coords = this.perspectiveCoords.slice().map(c => (
{
x: c[0] / fabric.devicePixelRatio,
y: c[1] / fabric.devicePixelRatio,
}
));
const minX = fabric.util.array.min(coords, 'x') || 0;
const minY = fabric.util.array.min(coords, 'y') || 0;
const maxX = fabric.util.array.max(coords, 'x') || 0;
const maxY = fabric.util.array.max(coords, 'y') || 0;
const width = Math.abs(maxX - minX);
const height = Math.abs(maxY - minY);
return {
left: minX,
top: minY,
width: width,
height: height,
};
}
/**
* #description This is modified version of the internal fabric function
* this subtracts the path offset from each path points
*/
_applyPointsOffset() {
for (let i = 0; i < this.perspectiveCoords.length; i++) {
const coord = this.perspectiveCoords[i];
coord[0] -= this.pathOffset.x;
coord[1] -= this.pathOffset.y;
}
}
/**
* #description generate the initial coordinates for warping, based on image dimensions
*
*/
getInitialPerspective() {
let w = this.getScaledWidth();
let h = this.getScaledHeight();
const perspectiveCoords = [
[0, 0], // top left
[w, 0], // top right
[w, h], // bottom right
[0, h], // bottom left
];
this.perspectiveCoords = perspectiveCoords;
const perspectiveFilter = new fabric.Image.filters.Perspective({
hasRelativeCoordinates: false,
pixelRatio: fabric.devicePixelRatio, // the Photo is already retina ready
perspectiveCoords
});
this.filters.push(perspectiveFilter);
this.applyFilters();
return perspectiveCoords;
}
};
/**
* Creates an instance of fabric.Photo from its object representation
* #static
* #param {Object} object Object to create an instance from
* #param {Function} callback Callback to invoke when an image instance is created
*/
fabric.Photo.fromObject = function(_object, callback) {
const object = fabric.util.object.clone(_object);
object.layout = _object.layout;
fabric.util.loadImage(object.src, function(img, isError) {
if (isError) {
callback && callback(null, true);
return;
}
fabric.Photo.prototype._initFilters.call(object, object.filters, function(filters) {
object.filters = filters || [];
fabric.Photo.prototype._initFilters.call(object, [object.resizeFilter], function(resizeFilters) {
object.resizeFilter = resizeFilters[0];
fabric.util.enlivenObjects([object.clipPath], function(enlivedProps) {
object.clipPath = enlivedProps[0];
var image = new fabric.Photo(img, object);
callback(image, false);
});
});
});
}, null, object.crossOrigin || 'anonymous');
};
const canvas = new fabric.Canvas(document.getElementById('canvas'), {
backgroundColor: 'white',
enableRetinaScaling: true,
});
function resizeCanvas() {
canvas.setWidth(window.innerWidth);
canvas.setHeight(window.innerHeight);
}
resizeCanvas();
window.addEventListener('resize', () => resizeCanvas(), false);
const photo = new fabric.Photo('data:image/jpeg;base64,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', {
left: canvas.getWidth() / 2,
top: canvas.getHeight() / 2,
originX: 'center',
originY: 'center',
// this make it not work
angle:90
});
canvas.add(photo);
canvas.setActiveObject(photo);

Related

Randomly Generating Curved/Wavy Paths

I have a massive image of a map that is much larger than the viewport and centered in the viewport, which can be explored by the user by dragging the screen. In order to create a parallax effect, I used a massive image of clouds in the foreground. As the user explores the map via dragging, both the background and foreground move in a parallax fashion. So far, so good.
However, what I really want to do is give the image of clouds a "default" movement that would be randomly generated on each page load, so that the clouds would always be moving, even if the user is not dragging. I know this can be done by animating the foreground along a path, but I am not exactly sure how to go about this.
How can I randomly generate irregularly curved or wavy paths on each page load?
Does anybody know of any algorithms that can do this?
I also use a copy of the previous answers to realize a simplified version of what I hinted at in the comments.
Use a random walk on the unit circle, that is on the angle, to determine a velocity vector that slowly but randomly changes and move forward using cubic Bezier patches.
var c = document.getElementById("c");
var ctx = c.getContext("2d");
var cw = c.width = 600;
var ch = c.height = 400;
var cx = cw / 4, cy = ch / 2;
var angVel = v.value;
var tension = t.value;
ctx.lineWidth = 4;
var npts = 60;
var dw = Array();
var xs = Array();
var ys = Array();
var vxs = Array();
var vys = Array();
function Randomize() {
for (var i = 0; i < npts; i++) {
dw[i] = (2*Math.random()-1);
}
}
function ComputePath() {
xs[0]=cx; ys[0]=cy;
var angle = 0;
for (var i = 0; i < npts; i++) {
vxs[i]=10*Math.cos(2*Math.PI*angle);
vys[i]=10*Math.sin(2*Math.PI*angle);
angle = angle + dw[i]*angVel;
}
for (var i = 1; i < npts; i++) {
xs[i] = xs[i-1]+3*(vxs[i-1]+vxs[i])/2;
ys[i] = ys[i-1]+3*(vys[i-1]+vys[i])/2;
}
}
function Draw() {
ctx.clearRect(0, 0, cw, ch);
ctx.beginPath();
ctx.moveTo(xs[0],ys[0]);
for (var i = 1; i < npts; i++) {
var cp1x = xs[i-1]+tension*vxs[i-1];
var cp1y = ys[i-1]+tension*vys[i-1];
var cp2x = xs[i]-tension*vxs[i];
var cp2y = ys[i]-tension*vys[i]
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, xs[i], ys[i]);
}
ctx.stroke();
}
Randomize();
ComputePath();
Draw();
r.addEventListener("click",()=>{
Randomize();
ComputePath();
Draw();
})
v.addEventListener("input",()=>{
angVel = v.value;
vlabel.innerHTML = ""+angVel;
ComputePath();
Draw();
})
t.addEventListener("input",()=>{
tension = t.value;
tlabel.innerHTML = ""+tension;
Draw();
})
canvas{border:1px solid}
<canvas id = 'c'></canvas>
<table>
<tr><td>angular velocity:</td><td> <input type="range" id="v" min ="0" max = "0.5" step = "0.01" value="0.2" /></td><td id="vlabel"></td></tr>
<tr><td>tension</td><td> <input type="range" id="t" min ="0" max = "1" step = "0.1" value="0.8" /></td><td id="tlabel"></td></tr>
<tr><td>remix</td><td> <button id="r"> + </button></td><td></td></tr>
</table>
If your question is: How can I randomly generate curved or wavy paths? this is how I would do it: I'm using inputs type range to change the value for amplitude and frequency, but you can set those values randomly on load.
I hope it helps.
var c = document.getElementById("c");
var ctx = c.getContext("2d");
var cw = c.width = 800;
var ch = c.height = 150;
var cx = cw / 2,
cy = ch / 2;
var amplitude = a.value;
var frequency = f.value;
ctx.lineWidth = 4;
function Draw() {
ctx.clearRect(0, 0, cw, ch);
ctx.beginPath();
for (var x = 0; x < cw; x++) {
y = Math.sin(x * frequency) * amplitude;
ctx.lineTo(x, y+cy);
}
ctx.stroke();
}
Draw();
a.addEventListener("input",()=>{
amplitude = a.value;
Draw();
})
f.addEventListener("input",()=>{
frequency = f.value;
Draw();
})
canvas{border:1px solid}
<canvas id = 'c'></canvas>
<p>frequency: <input type="range" id="f" min ="0.01" max = "0.1" step = "0.001" value=".05" /></p>
<p>amplitude: <input type="range" id="a" min ="1" max = "100" value="50" /></p>
I was impressed by the functionality to be able to draw canvases in the SO answers, so I "stole" enxaneta code snippet and played a bit with it (hope that is ok).
The idea is to generate several random points (xs, ys) and for each x from the path to interpolate the y as y = sum{ys_i*w_i}/sum{w_i}, where w_i is some interpolation weight as a function of x. For example w_i(x) = (xs_i - x)^(-2). Hope this makes sense - if this is of any interested I'll try to provide more details.
var c = document.getElementById("c");
var ctx = c.getContext("2d");
var cw = c.width = 600;
var ch = c.height = 150;
var cx = cw / 2,
cy = ch / 2;
var amplitude = a.value;
var frequency = f.value;
ctx.lineWidth = 4;
var npts = 20;
var xs = Array();
var ys = Array();
for (var i = 0; i < npts; i++) {
xs[i] = (cw/npts)*i;
ys[i] = 2.0*(Math.random()-0.5)*amplitude;
}
function Draw() {
ctx.clearRect(0, 0, cw, ch);
ctx.beginPath();
for (var x = 0; x < cw; x++) {
y = 0.0;
wsum = 0.0;
for (var i = -5; i <= 5; i++) {
xx = x;
ii = Math.round(x/xs[1]) + i;
if (ii < 0) { xx += cw; ii += npts; }
if (ii >= npts) { xx -= cw; ii -= npts; }
w = Math.abs(xs[ii] - xx);
w = Math.pow(w, frequency);
y += w*ys[ii];
wsum += w;
}
y /= wsum;
//y = Math.sin(x * frequency) * amplitude;
ctx.lineTo(x, y+cy);
}
ctx.stroke();
}
Draw();
a.addEventListener("input",()=>{
amplitude = a.value;
for (var i = 0; i < npts; i++) {
xs[i] = (cw/npts)*i;
ys[i] = 2.0*(Math.random()-0.5)*amplitude;
}
Draw();
})
f.addEventListener("input",()=>{
frequency = f.value;
Draw();
})
canvas{border:1px solid}
<canvas id = 'c'></canvas>
<p>amplitude: <input type="range" id="a" min ="1" max = "100" value="50" /></p>
<p>frequency: <input type="range" id="f" min ="-10" max = "1" step = "0.1" value="-2" hidden/></p>
Deterministic random paths
Storing paths for random movements is not needed. Also random is another way of being very complex, and for humans it does not take much complexity to look randoms.
Thus with a little randomness to add to complexity you can make the appearance of the infinite non repeating sequence that and be rewound, stopped, slowed down speed up, and be fully deterministic and requiring only a single value to store.
Complex cycles.
To move a point in a circle around a center you can use sin and cos.
For example a point x,y and you want to move in a ball around that point at a distance of dist and a rate once a second. Example in snippet.
var px = 100; // point of rotation.
var py = 100;
const RPS = 1; // Rotations Per Second
const dist = 50; // distance from point
const radius = 25; // circle radius
function moveObj(time) { // Find rotated point and draw
time = (time / 1000) * PI2 * RPS; // convert the time to rotations per secon
const xx = Math.cos(time) * dist;
const yy = Math.sin(time) * dist;
drawCircle(xx, yy)
}
// Helpers
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);
function drawCircle(x,y,r = radius) {
ctx.setTransform(1,0,0,1,px,py);
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(x,y,r,0,PI2);
ctx.fill();
}
function mainLoop(time) {
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
moveObj(time);
requestAnimationFrame(mainLoop);
}
const PI = Math.PI;
const PI2 = PI * 2;
canvas {
background : #8AF;
border : 1px solid black;
}
<canvas id="canvas" width="200" height="200"></canvas>
Next let's move the point around which we rotate, using the method above.
Then for the ball we can change the phase of the rotation in x from the rotation in y. This means that the ball rotating around the now rotating point, and the balls rotating axis are out of phase.
The result is a more complex movements.
var px = 100; // point of rotation.
var py = 100;
const RPS_P = 0.1; // point Rotations Per Second 0.1 every 10 seconds
const RPS_X = 1; // Rotations Per Second in x axis of circle
const RPS_Y = 0.8; // Rotations Per Second in y axis of circle
const dist_P = 30; // distance from center point is
const dist = 50; // distance from point
const radius = 25; // circle radius
function moveObj(time) { // Find rotated point and draw
var phaseX = (time / 1000) * PI2 * RPS_X;
var phaseY = (time / 1000) * PI2 * RPS_Y;
const xx = Math.cos(phaseX) * dist;
const yy = Math.sin(phaseY) * dist;
drawCircle(xx, yy)
}
function movePoint(time) { // move point around center
time = (time / 1000) * PI2 * RPS_P;
px = 100 + Math.cos(time) * dist_P;
py = 100 + Math.sin(time) * dist_P;
}
// Helpers
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);
function drawCircle(x,y,r = radius) {
ctx.setTransform(1,0,0,1,px,py);
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(x,y,r,0,PI2);
ctx.fill();
}
function mainLoop(time) {
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
movePoint(time);
moveObj(time);
requestAnimationFrame(mainLoop);
}
const PI = Math.PI;
const PI2 = PI * 2;
canvas {
background : #8AF;
border : 1px solid black;
}
<canvas id="canvas" width="200" height="200"></canvas>
We can continue to add out of phase rotations. In the next example we now rotate the rotation point around the center, add out of phase rotation to that point and finally draw the ball with its out of phase rotation.
var px = 100; // point of rotation.
var py = 100;
const RPS_C_X = 0.43; // Rotation speed X of rotating rotation point
const RPS_C_Y = 0.47; // Rotation speed X of rotating rotation point
const RPS_P_X = 0.093; // point Rotations speed X
const RPS_P_Y = 0.097; // point Rotations speed Y
const RPS_X = 1; // Rotations Per Second in x axis of circle
const RPS_Y = 0.8; // Rotations Per Second in y axis of circle
const dist_C = 20; // distance from center point is
const dist_P = 30; // distance from center point is
const dist = 30; // distance from point
const radius = 25; // circle radius
function moveObj(time) { // Find rotated point and draw
var phaseX = (time / 1000) * PI2 * RPS_X;
var phaseY = (time / 1000) * PI2 * RPS_Y;
const xx = Math.cos(phaseX) * dist;
const yy = Math.sin(phaseY) * dist;
drawCircle(xx, yy)
}
function movePoints(time) { // Move the rotating pointe and rotate the rotation point
// around that point
var phaseX = (time / 1000) * PI2 * RPS_C_X;
var phaseY = (time / 1000) * PI2 * RPS_C_Y;
px = 100 + Math.cos(phaseX) * dist_C;
py = 100 + Math.sin(phaseY) * dist_C;
phaseX = (time / 1000) * PI2 * RPS_P_X;
phaseY = (time / 1000) * PI2 * RPS_P_Y;
px = px + Math.cos(phaseX) * dist_P;
py = py + Math.sin(phaseY) * dist_P;
}
// Helpers
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);
function drawCircle(x,y,r = radius) {
ctx.setTransform(1,0,0,1,px,py);
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(x,y,r,0,PI2);
ctx.fill();
}
function mainLoop(time) {
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
movePoints(time);
moveObj(time);
requestAnimationFrame(mainLoop);
}
const PI = Math.PI;
const PI2 = PI * 2;
canvas {
background : #8AF;
border : 1px solid black;
}
<canvas id="canvas" width="200" height="200"></canvas>
So now we have a very complex rotation. However as it is set to the time, you can repeat the movement by just setting the time back to the start. You don't need to store a long complex path.
Add a little random
You may see some repeating movement but if you make the phases of each axis a prime then the repeat time is the product of all the primes.
If you want many objects each with a different movement you can randomise the rotation rates and many more properties.
Javascript does not have a seeded random generator. However you can create one. With a seeded random generator you can us the seed to generate a random object. But if you use that seed again you get the same object. In the example below I us a seed from 0 to 10000000 to create a cloud. That means there are 10000000 unique clouds, but all repeatable.
Example of deterministic random clouds
Restart and it will repeat exactly the same. To change it to non deterministic random just add randSeed(Math.random() * 100000 | 0)
const seededRandom = (() => {
var seed = 1;
return { max : 2576436549074795, reseed (s) { seed = s }, random () { return seed = ((8765432352450986 * seed) + 8507698654323524) % this.max }}
})();
const randSeed = (seed) => seededRandom.reseed(seed|0);
const randSI = (min = 2, max = min + (min = 0)) => (seededRandom.random() % (max - min)) + min;
const randS = (min = 1, max = min + (min = 0)) => (seededRandom.random() / seededRandom.max) * (max - min) + min;
const randSPow = (min, max = min + (min = 0), p = 2) => (max + min) / 2 + (Math.pow(seededRandom.random() / seededRandom.max, p) * (max - min) * 0.5) * (randSI(2) < 1 ? 1 : -1);
const ctx = canvas.getContext("2d");
const W = ctx.canvas.width;
const H = ctx.canvas.height;
const DIAG = (W * W + H * H) ** 0.5;
const colors = {
dark : {
minRGB : [100 * 0.6,200 * 0.6,240 * 0.6],
maxRGB : [255 * 0.6,255 * 0.6,255 * 0.6],
},
light : {
minRGB : [100,200,240],
maxRGB : [255,255,255],
},
}
const getCol = (pos, range) => "rgba(" +
((range.maxRGB[0] - range.minRGB[0]) * pos + range.minRGB[0] | 0) + "," +
((range.maxRGB[1] - range.minRGB[1]) * pos + range.minRGB[1] | 0) + "," +
((range.maxRGB[2] - range.minRGB[2]) * pos + range.minRGB[2] | 0) + "," +(pos * 0.2 + 0.8) + ")";
const Cloud = {
x : 0,
y : 0,
dir : 0, // in radians
wobble : 0,
wobble1 : 0,
wSpeed : 0,
wSpeed1 : 0,
mx : 0, // Move offsets
my : 0,
seed : 0,
size : 2,
detail : null,
reset : true, // when true could resets
init() {
this.seed = randSI(10000000);
this.reset = false;
var x,y,r,dir,dist,f;
if (this.detail === null) { this.detail = [] }
else { this.detail.length = 0 }
randSeed(this.seed);
this.size = randSPow(2, 8); // The pow add bias to smaller values
var col = (this.size -2) / 6;
this.col1 = getCol(col,colors.dark)
this.col2 = getCol(col,colors.light)
var flufCount = randSI(5,15);
while (flufCount--) {
x = randSI(-this.size * 8, this.size * 8);
r = randS(this.size * 2, this.size * 8);
dir = randS(Math.PI * 2);
dist = randSPow(1) * r ;
this.detail.push(f = {x,r,y : 0,mx:0,my:0, move : randS(0.001,0.01), phase : randS(Math.PI * 2)});
f.x+= Math.cos(dir) * dist;
f.y+= Math.sin(dir) * dist;
}
this.xMax = this.size * 12 + this.size * 10 + this.size * 4;
this.yMax = this.size * 10 + this.size * 4;
this.wobble = randS(Math.PI * 2);
this.wSpeed = randS(0.01,0.02);
this.wSpeed1 = randS(0.01,0.02);
const aOff = randS(1) * Math.PI * 0.5 - Math.PI *0.25;
this.x = W / 2 - Math.cos(this.dir+aOff) * DIAG * 0.7;
this.y = H / 2 - Math.sin(this.dir+aOff) * DIAG * 0.7;
clouds.sortMe = true; // flag that coulds need resort
},
move() {
var dx,dy;
this.dir = gTime / 10000;
if(this.reset) { this.init() }
this.wobble += this.wSpeed;
this.wobble1 += this.wSpeed1;
this.mx = Math.cos(this.wobble) * this.size * 4;
this.my = Math.sin(this.wobble1) * this.size * 4;
this.x += dx = Math.cos(this.dir) * this.size / 5;
this.y += dy = Math.sin(this.dir) * this.size / 5;
if (dx > 0 && this.x > W + this.xMax ) { this.reset = true }
else if (dx < 0 && this.x < - this.xMax ) { this.reset = true }
if (dy > 0 && this.y > H + this.yMax) { this.reset = true }
else if (dy < 0 && this.y < - this.yMax) { this.reset = true }
},
draw(){
const s = this.size;
const s8 = this.size * 8;
ctx.fillStyle = this.col1;
ctx.setTransform(1,0,0,1,this.x+ this.mx,this.y +this.my);
ctx.beginPath();
for (const fluf of this.detail) {
fluf.phase += fluf.move + Math.sin(this.wobble * this.wSpeed1) * 0.02 * Math.cos(fluf.phase);
fluf.mx = Math.cos(fluf.phase) * fluf.r / 2;
fluf.my = Math.sin(fluf.phase) * fluf.r / 2;
const x = fluf.x + fluf.mx;
const y = fluf.y + fluf.my;
ctx.moveTo(x + fluf.r + s, y);
ctx.arc(x,y,fluf.r+ s,0,Math.PI * 2);
}
ctx.fill();
ctx.fillStyle = this.col2;
ctx.globalAlpha = 0.5;
ctx.beginPath();
for (const fluf of this.detail) {
const x = fluf.x + fluf.mx - s;
const y = fluf.y + fluf.my - s;
ctx.moveTo(x + fluf.r, y);
ctx.arc(x,y,fluf.r,0,Math.PI * 2);
}
ctx.fill();
ctx.globalAlpha = 0.6;
ctx.beginPath();
for (const fluf of this.detail) {
const x = fluf.x + fluf.mx - s * 1.4;
const y = fluf.y + fluf.my - s * 1.4;
ctx.moveTo(x + fluf.r * 0.8, y);
ctx.arc(x,y,fluf.r* 0.8,0,Math.PI * 2);
}
ctx.fill();
ctx.globalAlpha = 1;
}
}
function createCloud(size){ return {...Cloud} }
const clouds = Object.assign([],{
move() { for(const cloud of this){ cloud.move() } },
draw() { for(const cloud of this){ cloud.draw() } },
sortMe : true, // if true then needs to resort
resort() {
this.sortMe = false;
this.sort((a,b)=>a.size - b.size);
}
});
for(let i = 0; i < 15; i ++) { clouds.push(createCloud(40)) }
requestAnimationFrame(mainLoop)
var gTime = 0;
function mainLoop() {
gTime += 16;
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
if(clouds.sortMe) { clouds.resort() }
clouds.move();
clouds.draw();
requestAnimationFrame(mainLoop);
}
body { padding : 0px; margin : 0px;}
canvas {
background : rgb(60,120,148);
border : 1px solid black;
}
<canvas id="canvas" width="600" height="200"></canvas>

THREE.js Secondary camera rotated with head rotation in VR

I'm using THREE.js and Aframe ( in Exokit ) together and I have a component for a "selfie camera". I have a weird issue that when i enter VR the camera rotation is taken over by the head rotation. I understand how the camera rotation works has changed in recent versions of THREE.js ( ArrayCamera ) but I assumed that only affected the main camera and not all cameras in the scene.
Below is my hacky component that works fine in 2D mode but in VR it messes up. The worst thing about it is im fine with it being linked to the head, the camera itself is a child object of the main camera anyway so it appears in front of the users face when opened and is moved with the head rotation - but its off angle when in VR like its pointing down and to the left a bit.
Here are some screenshots that hopefully demonstrate the issue:
Edit: need 10 rep to post images so here are urls instead
2D Mode
VR Mode
Any help much appreciated!!
AFRAME.registerComponent('selfie-camera', {
schema:{
resolution:{type:'int',default:512},
fov:{type:'int',default:100},
aspect:{type:'number',default:1.5},
near:{type:'number',default:0.001},
far:{type:'number',default:1000}
},
init() {
this.el.addEventListener('loaded',()=>{
this.renderTarget = new THREE.WebGLRenderTarget(this.data.resolution*1.5, this.data.resolution,{ antialias: true });
this.el.getObject3D('mesh').material.map = this.renderTarget.texture;
this.cameraContainer = new THREE.Object3D();
this.el.object3D.add( this.cameraContainer );
this.el.takePicture = this.takePicture.bind(this);
this.el.setSide = this.setSide.bind(this);
this.wider = 1.5;
this.photoMultiplier = 2;
this.canvas = document.createElement('canvas');
});
this.testQuat = new THREE.Quaternion();
this.el.open = this.open.bind(this);
this.el.close = this.close.bind(this);
},
open(){
this.camera = new THREE.PerspectiveCamera( this.data.fov, this.data.aspect, this.data.near, this.data.far );
this.cameraContainer.add(this.camera);
new TWEEN.Tween(this.el.getAttribute('scale'))
.to(new THREE.Vector3(1,1,1), 650)
.easing(TWEEN.Easing.Exponential.Out).start();
},
close(){
new TWEEN.Tween(this.el.getAttribute('scale'))
.to(new THREE.Vector3(0.0000001,0.0000001,0.0000001), 200)
.onComplete(()=>{
this.cameraContainer.remove(this.camera);
delete this.camera;
})
.easing(TWEEN.Easing.Exponential.Out).start();
},
tick(){
if(this.camera){
this.camera.getWorldQuaternion(this.testQuat);
console.log(this.camera.quaternion);
}
this.el.getObject3D('mesh').material.visible = false;
if(this.isTakingPicture) {
this.renderTarget.setSize(this.data.resolution * this.wider * this.photoMultiplier, this.data.resolution * this.photoMultiplier);
}
this.el.sceneEl.renderer.render( this.el.sceneEl.object3D, this.camera, this.renderTarget );
if(this.isTakingPicture){
this.isTakingPicture = false;
this.pictureResolve(this.createImageFromTexture());
this.renderTarget.setSize(this.data.resolution * this.wider, this.data.resolution);
}
this.el.getObject3D('mesh').material.visible = true;
},
setSide(isFront){
let _this = this;
new TWEEN.Tween({y:this.cameraContainer.rotation.y})
.to({y:isFront?Math.PI:0}, 350)
.onUpdate(function(){
_this.cameraContainer.rotation.y = this.y;
})
.easing(TWEEN.Easing.Exponential.Out).start();
},
takePicture(){
return new Promise(resolve=>{
this.isTakingPicture = true;
this.pictureResolve = resolve;
})
},
createImageFromTexture() {
let width = this.data.resolution*this.wider*this.photoMultiplier,
height = this.data.resolution*this.photoMultiplier;
let pixels = new Uint8Array(4 * width * height);
this.el.sceneEl.renderer.readRenderTargetPixels(this.renderTarget, 0, 0, width, height, pixels);
pixels = this.flipPixelsVertically(pixels, width, height);
let imageData = new ImageData(new Uint8ClampedArray(pixels), width, height);
this.canvas.width = width;
this.canvas.height = height;
let context = this.canvas.getContext('2d');
context.putImageData(imageData, 0, 0);
return this.canvas.toDataURL('image/jpeg',100);
},
flipPixelsVertically: function (pixels, width, height) {
let flippedPixels = pixels.slice(0);
for (let x = 0; x < width; ++x) {
for (let y = 0; y < height; ++y) {
flippedPixels[x * 4 + y * width * 4] = pixels[x * 4 + (height - y) * width * 4];
flippedPixels[x * 4 + 1 + y * width * 4] = pixels[x * 4 + 1 + (height - y) * width * 4];
flippedPixels[x * 4 + 2 + y * width * 4] = pixels[x * 4 + 2 + (height - y) * width * 4];
flippedPixels[x * 4 + 3 + y * width * 4] = pixels[x * 4 + 3 + (height - y) * width * 4];
}
}
return flippedPixels;
}
});
You have to disable VR before rendering:
var renderer = this.el.sceneEl.renderer;
var vrEnabled = renderer.vr.enabled;
renderer.vr.enabled = false;
renderer.render(this.el.sceneEl.object3D, this.camera, this.renderTarget);
renderer.vr.enabled = vrEnabled;

How to create rotation on perspective camera on Three.js?

I have build a little 3D-TileMap in Three.js.
Currently, i have problems with my PerspectiveCamera. I wan't to add some camera handling like Map rotating or zooming. The zooming always works, here i'm only use the field of view and mousewheel.
But how i can implement a rotating of my map? When i'm using the coordinates of camera to modify x, y or z, i've misunderstand the calculation.
Here is my current work:
function Input(renderer, camera) {
var press = false
var sensitivity = 0.2
renderer.domElement.addEventListener('mousemove', event => {
if(!press){ return }
camera.position.x += event.movementX * sensitivity
camera.position.y += event.movementY * sensitivity
camera.position.z += event.movementY * sensitivity / 10
})
renderer.domElement.addEventListener('mousedown', () => { press = true })
renderer.domElement.addEventListener('mouseup', () => { press = false })
renderer.domElement.addEventListener('mouseleave', () => { press = false })
renderer.domElement.addEventListener('mousewheel', event => {
// Add MIN/MAX LIMITS
const ratio = camera.position.y / camera.position.z
camera.position.y -= (event.wheelDelta * sensitivity * ratio)
camera.position.z -= (event.wheelDelta * sensitivity)
camera.updateProjectionMatrix()
})
}
var controls;
const Type = 'WebGL'; // WebGL or Canvas
var _width, _height, CUBE_SIZE, GRID, TOTAL_CUBES, WALL_SIZE, HALF_WALL_SIZE,
MAIN_COLOR, SECONDARY_COLOR, cubes, renderer, camera, scene, group
var clock = new THREE.Clock();
clock.start();
var FOV = 45;
_width = window.innerWidth
_height = window.innerHeight
CUBE_SIZE = 80 /* width, height */
GRID = 12 /* cols, rows */
TOTAL_CUBES = (GRID * GRID)
WALL_SIZE = (GRID * CUBE_SIZE)
HALF_WALL_SIZE = (WALL_SIZE / 2)
MAIN_COLOR = 0xFFFFFF
SECONDARY_COLOR = 0x222222
cubes = []
var directions = [];
var normalized = [];
switch(Type) {
case 'WebGL':
renderer = new THREE.WebGLRenderer({antialias: true})
break;
case 'Canvas':
renderer = new THREE.CanvasRenderer({antialias: true})
break;
}
camera = new THREE.PerspectiveCamera(FOV, (_width / _height), 0.1, 10000)
scene = new THREE.Scene()
group = new THREE.Object3D()
/* -- -- */
setupCamera(0, 0, 800)
setupBox(group)
setupFloor(group)
setupCubes(group)
setupLights(group)
group.position.y = 10
group.rotation.set(-60 * (Math.PI/180), 0, -45 * (Math.PI/180))
scene.add(group)
setupRenderer(document.body)
window.addEventListener('resize', resizeHandler, false)
new Input(renderer, camera);
/* -- -- */
function resizeHandler() {
_width = window.innerWidth
_height = window.innerHeight
renderer.setSize(_width, _height)
camera.aspect = _width / _height
camera.updateProjectionMatrix()
}
/* -- CAMERA -- */
function setupCamera(x, y, z) {
camera.position.set(x, y, z)
scene.add(camera)
}
/* -- BOX -- */
function setupBox(parent) {
var i, boxesArray, geometry, material
boxesArray = []
geometry = new THREE.BoxGeometry(WALL_SIZE, WALL_SIZE, 0.05)
geometry.faces[8].color.setHex(SECONDARY_COLOR)
geometry.faces[9].color.setHex(SECONDARY_COLOR)
geometry.colorsNeedUpdate = true
material = new THREE.MeshBasicMaterial({
color : MAIN_COLOR,
vertexColors : THREE.FaceColors,
overdraw: 0.5
})
for (i = 0; i < 5; i++) {
boxesArray.push(new THREE.Mesh(geometry, material))
}
// back
boxesArray[0].position.set(0, HALF_WALL_SIZE, -HALF_WALL_SIZE)
boxesArray[0].rotation.x = 90 * (Math.PI/180)
// right
boxesArray[1].position.set(HALF_WALL_SIZE, 0, -HALF_WALL_SIZE)
boxesArray[1].rotation.y = -90 * (Math.PI/180)
// front
boxesArray[2].position.set(0, -HALF_WALL_SIZE, -HALF_WALL_SIZE)
boxesArray[2].rotation.x = -90 * (Math.PI/180)
// left
boxesArray[3].position.set(-HALF_WALL_SIZE, 0, -HALF_WALL_SIZE)
boxesArray[3].rotation.y = 90 * (Math.PI/180)
// bottom
boxesArray[4].position.set(0, 0, -WALL_SIZE)
boxesArray.forEach(function(box) {
box.renderOrder = 1;
parent.add(box)
});
}
/* -- FLOOR -- */
function setupFloor(parent) {
var i, tilesArray, geometry, material
tilesArray = []
geometry = new THREE.PlaneBufferGeometry(WALL_SIZE, WALL_SIZE)
material = new THREE.MeshLambertMaterial({
color : MAIN_COLOR,
overdraw: 1
})
for (i = 0; i < 8; i++) {
tilesArray.push(new THREE.Mesh(geometry, material))
}
tilesArray[0].position.set(-WALL_SIZE, WALL_SIZE, 0)
tilesArray[1].position.set(0, WALL_SIZE, 0)
tilesArray[2].position.set(WALL_SIZE, WALL_SIZE, 0)
tilesArray[3].position.set(-WALL_SIZE, 0, 0)
tilesArray[4].position.set(WALL_SIZE, 0, 0)
tilesArray[5].position.set(-WALL_SIZE, -WALL_SIZE, 0)
tilesArray[6].position.set(0, -WALL_SIZE, 0)
tilesArray[7].position.set(WALL_SIZE, -WALL_SIZE, 0)
tilesArray.forEach(function(tile) {
tile.receiveShadow = true
tile.renderOrder = 4;
parent.add(tile)
})
}
/* -- CUBES --*/
function setupCubes(parent) {
var i, geometry, material, x, y, row, col
geometry = new THREE.BoxGeometry(CUBE_SIZE, CUBE_SIZE, 0.05)
material = new THREE.MeshPhongMaterial( {
map: new THREE.TextureLoader().load('http://ak.game-socket.de/assets/grass.png'),
normalMap: new THREE.TextureLoader().load('http://ak.game-socket.de/assets/paper_low_nmap.png'),
overdraw: 1,
depthTest: true,
depthWrite: true
} );
x = 0
y = 0
row = 0
col = 0
for (i = 0; i < TOTAL_CUBES; i++) {
cubes.push(new THREE.Mesh(geometry, material))
if ((i % GRID) === 0) {
col = 1
row++
} else col++
x = -(((GRID * CUBE_SIZE) / 2) - ((CUBE_SIZE) * col) + (CUBE_SIZE/2))
y = -(((GRID * CUBE_SIZE) / 2) - ((CUBE_SIZE) * row) + (CUBE_SIZE/2))
cubes[i].position.set(x, y, 0)
}
cubes.forEach(function(cube, index) {
if(index % 2 == 0) {
directions[index] = -1;
normalized[index] = false;
} else {
directions[index] = 1;
normalized[index] = true;
}
cube.castShadow = true
cube.receiveShadow = true
cube.rotation.x = 0;
cube.renderOrder = 3;
cube.doubleSide = true;
parent.add(cube)
})
}
/* -- LIGHTS -- */
function setupLights(parent) {
var light, soft_light
light = new THREE.DirectionalLight(MAIN_COLOR, 1.25)
soft_light = new THREE.DirectionalLight(MAIN_COLOR, 1.5)
light.position.set(-WALL_SIZE, -WALL_SIZE, CUBE_SIZE * GRID)
light.castShadow = true
soft_light.position.set(WALL_SIZE, WALL_SIZE, CUBE_SIZE * GRID)
parent.add(light).add(soft_light)
}
/* -- RENDERER -- */
function setupRenderer(parent) {
renderer.setSize(_width, _height)
renderer.setClearColor(MAIN_COLOR, 1.0);
parent.appendChild(renderer.domElement)
}
var speed = 0.003;
var reach = 40;
function render() {
var delta = clock.getDelta();
requestAnimationFrame(render);
cubes.forEach(function(cube, index) {
cube.castShadow = true
cube.receiveShadow = true
if(directions[index] >= 1) {
++directions[index];
if(directions[index] >= reach) {
directions[index] = -1
}
cube.rotation.x += speed;
} else if(directions[index] <= -1) {
--directions[index];
if(directions[index] <= -reach) {
directions[index] = 1
}
cube.rotation.x -= speed;
}
});
renderer.render(scene, camera)
}
render();
html, body, canvas {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
display: block;
}
<script src="https://rawcdn.githack.com/mrdoob/three.js/dev/build/three.min.js"></script>
<script src="https://rawcdn.githack.com/mrdoob/three.js/dev/examples/js/renderers/CanvasRenderer.js"></script>
<script src="https://rawcdn.githack.com/mrdoob/three.js/dev/examples/js/renderers/Projector.js"></script>
<script src="https://rawcdn.githack.com/mrdoob/three.js/dev/examples/js/controls/TrackballControls.js"></script>
<script src="https://rawcdn.githack.com/mrdoob/three.js/dev/examples/js/shaders/ParallaxShader.js"></script>
By option, i don't want, that the rotating/zooming reach the end of the map - The user is not able to look under the map, as example.

How to remove merged Geometry

For performance reasons I merged geometry. I have tens of thousands of cubes to display. I have that working with reasonable performance.
Now I have to deal with removing some. I almost have it but can't figure out how to make this work, so I cut my code up to make this complete sample.
In the onDocumentMouseDown function when a cube is clicked on I try to remove it. And it sort of does. But instead of removing one cube it removes two. (Then it basically acts worse) It removes the one I pointed at and the next one I added.
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<title>Measurement</title>
<style>
body { margin: 0; }
</style>
</head>
<body>
<div id="canvas_container" style="position: absolute; left:0px; top:0px; touch-action:none;"></div>
<div id="MessageDisplay1" style="position: absolute; top: 50px; left: 50px; background-color: black; opacity: 0.8; color:white; touch-action:none;"></div>
<div id="MessageDisplay" style="position: absolute; top: 50px; left: 200px; background-color: black; opacity: 0.8; color:white; touch-action:none;"></div>
<script src="js/three.js"></script>
<script>
// player motion parameters
var motioncontrol = {
airborne: false,
bumpposition: 5.0,
bumpdegrees: 4.0,
rotationanglezx: 0,
tiltangle: 0,
distancePointZ : 10000.0,
distancePointY: 100.0,
position : new THREE.Vector3(), velocity : new THREE.Vector3(),
rotation: new THREE.Vector3(), spinning: new THREE.Vector2(),
prevposition : new THREE.Vector3(),
prevrotation : new THREE.Vector3()
};
var mouseDown = 0;
var mouse = new THREE.Vector2();
var raycaster = new THREE.Raycaster();
var INTERSECTED;
motioncontrol.position.y = 15;
motioncontrol.position.x = 0;
motioncontrol.position.z = 0;
motioncontrol.rotation.x = 0;
motioncontrol.rotation.z = motioncontrol.distancePointZ * Math.cos(0);
motioncontrol.rotation.x = motioncontrol.distancePointZ * Math.sin(0);
motioncontrol.prevposition.copy(motioncontrol.position);
motioncontrol.prevrotation.copy(motioncontrol.rotation);
// Our Javascript will go here.
var domContainer = null;
domContainer = document.getElementById("canvas_container");
var camera = new THREE.PerspectiveCamera( 50, window.innerWidth/window.innerHeight, 0.1, 5000 );
var aspectratio = window.innerWidth/window.innerHeight;
var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
domContainer.appendChild(renderer.domElement);
var materials = THREE.ImageUtils.loadTexture('texture/sky.jpg');
addEventListener('mousemove', onDocumentMouseMove, false);
domContainer.addEventListener('mousedown', onDocumentMouseDown, false);
domContainer.addEventListener('mouseup', onDocumentMouseUp, false);
var scene = new THREE.Scene();
scene.add(camera);
window.addEventListener('resize', resize, false);
camera.position.x = 0;
camera.position.y = 100;
camera.position.z = -100;
camera.rotation.y = Math.PI / 180.0 * 90;
var directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
directionalLight.position.set(0, -10, 0);
scene.add(directionalLight);
directionalLight = new THREE.DirectionalLight(0xffffff, 3.0);
directionalLight.position.set(0, -50,-1000);
scene.add(directionalLight);
directionalLight = new THREE.DirectionalLight(0xffffff, 3.0);
directionalLight.position.set(0, -50, 1000);
scene.add(directionalLight);
directionalLight = new THREE.DirectionalLight(0xffffff, 3.0);
directionalLight.position.set(-200, -10, 0);
scene.add(directionalLight);
directionalLight = new THREE.DirectionalLight(0xffffff, 3.0);
directionalLight.position.set(200, -10, 0);
scene.add(directionalLight);
addGround(scene);
// array of unsorted geometries.
var CubeGeometryArray = new Array();
var CubeGeometryTier1 = new THREE.Geometry();
var CubeGeometryTier2 = new THREE.Geometry();
var CubeGeometryTier3 = new THREE.Geometry();
var CubeGeometryTier4 = new THREE.Geometry();
var CubeGeometryTier5 = new THREE.Geometry();
// array of materials
var CubeMaterials = new Array();
// array of meshes used for hit testing.
var CubeArray = new Array();
var cMaterialCount = 0;
var Cube20Mesh;
var Cube40Mesh;
var CubesLoaded = false;
LoadCubeMaterial();
LoadCubeMeshs();
var controls;
function resize()
{
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
};
function onMouseWheel(event)
{
var delta = 0;
if ( event.wheelDelta !== undefined ) {
// WebKit / Opera / Explorer 9
delta = event.wheelDelta;
} else if ( event.detail !== undefined ) {
// Firefox
delta = - event.detail;
}
if ( delta > 0 ) { // forward
var angle = Math.atan2(motioncontrol.rotation.x, motioncontrol.rotation.z);
motioncontrol.position.z += motioncontrol.bumpposition * Math.cos(angle);
motioncontrol.position.x += motioncontrol.bumpposition * Math.sin(angle);
} else if ( delta < 0 ) {
var angle = Math.atan2(motioncontrol.rotation.x, motioncontrol.rotation.z);
angle += Math.PI;
motioncontrol.position.z += motioncontrol.bumpposition * Math.cos(angle);
motioncontrol.position.x += motioncontrol.bumpposition * Math.sin(angle);
}
};
function onDocumentMouseMove(event)
{
event.preventDefault();
if (mouseDown > 0) {
if (((event.clientX / window.innerWidth) * 2 - 1) > mouse.x) {
motioncontrol.rotationanglezx -= motioncontrol.bumpdegrees;
if (motioncontrol.rotationanglezx < 0)
motioncontrol.rotationanglezx += 360;
var angle = (Math.PI / 180.0) * motioncontrol.rotationanglezx;
motioncontrol.rotation.x = motioncontrol.distancePointZ * Math.cos(angle) - motioncontrol.distancePointZ * Math.sin(angle);
motioncontrol.rotation.z = motioncontrol.distancePointZ * Math.sin(angle) + motioncontrol.distancePointZ * Math.cos(angle);
}
if (((event.clientX / window.innerWidth) * 2 - 1) < mouse.x) {
motioncontrol.rotationanglezx += motioncontrol.bumpdegrees;
if (motioncontrol.rotationanglezx > 360)
motioncontrol.rotationanglezx -= 360;
var angle = (Math.PI / 180.0) * motioncontrol.rotationanglezx;
motioncontrol.rotation.x = motioncontrol.distancePointZ * Math.cos(angle) - motioncontrol.distancePointZ * Math.sin(angle);
motioncontrol.rotation.z = motioncontrol.distancePointZ * Math.sin(angle) + motioncontrol.distancePointZ * Math.cos(angle);
}
}
};
function onDocumentMouseDown(event)
{
++mouseDown;
event.preventDefault();
var mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
intersects = raycaster.intersectObjects(CubeArray);
if(intersects.length > 0)
{
if(intersects[0].object.name != null)
{
var offset = intersects[0].object.name * 8;
var offsetfaces = intersects[0].object.name * 12;
var index = intersects[0].object.name;
var selectedObject = scene.getObjectByName("Tier1");
scene.remove(selectedObject);
CubeArray.splice(index, 1);
CubeGeometryTier1 = CubeGeometryArray[0];
CubeGeometryTier1.vertices.splice(offset, 8);
CubeGeometryTier1.faces.splice(offsetfaces, 12);
CubeGeometryTier1.faceVertexUvs[0].splice(offsetfaces, 12);
CubeGeometryArray[0] = CubeGeometryTier1.clone();
CubeGeometryTier1.sortFacesByMaterialIndex();
var cmesh = new THREE.Mesh(CubeGeometryTier1, new THREE.MeshFaceMaterial(CubeMaterials));
cmesh.matrixAutoUpdate = false;
cmesh.updateMatrix();
cmesh.name = "Tier1";
scene.add(cmesh);
}
else
INTERSECTED = null;
}
};
function onDocumentMouseUp(event) {
mouseDown = 0;
event.preventDefault();
}
function addGround(scene1)
{
var materialg = new THREE.MeshBasicMaterial( { color: 0x333333 , side: THREE.BackSide } );
var groundmesh = new THREE.Mesh(new THREE.PlaneGeometry(9000, 4500, 1), materialg);
groundmesh.receiveShadow = true;
groundmesh.rotation.x = Math.PI / 180.0 * 90;
groundmesh.position.set(0, 0, 0);
groundmesh.name = "asphalt";
scene1.add(groundmesh);
};
function writeToScreen(message)
{
var pre = document.getElementById("MessageDisplay");
pre.style.wordWrap = "break-word";
pre.innerHTML = message;
}
function writeToScreen2(message)
{
var pre = document.getElementById("MessageDisplay1");
pre.style.wordWrap = "break-word";
pre.innerHTML = message;
}
function addCube20(name, x1,z1,azimuth,height,add)
{
var cube;
if (add)
{
if (height > 5)
height = 5;
cube = Cube20Mesh.clone();
cube.visible = true;
cube.receiveShadow = true;
cube.position.set(x1, ((height - 1) * 8.5) + 4.25, z1);
cube.rotation.y = (Math.PI / 180.0) * azimuth;
cube.name = name;
cube.updateMatrix();
AddCubeGeometry(cube.geometry, cube.matrix, height);
cube.matrixWorld = cube.matrix;
CubeArray.push(cube); // kept for hit test
}
};
function addCube40(name, x1, z1, azimuth,height,add)
{
var cube;
if (add)
{
if (height > 5)
height = 1;
cube = Cube40Mesh.clone();
cube.visible = true;
cube.receiveShadow = true;
cube.position.set(x1, ((height - 1) * 8.5) + 4.25, z1);
cube.rotation.y = (Math.PI / 180.0) * azimuth;
cube.name = name;
cube.updateMatrix();
AddCubeGeometry(cube.geometry, cube.matrix, height + 5);
cube.matrixWorld = cube.matrix;
CubeArray.push(cube); // kept for hit test
}
};
function LoadCubeMeshs()
{
var CubeGeometry20 = new THREE.BoxGeometry(20, 8.5, 9.5);
var CubeGeometry40 = new THREE.BoxGeometry(40, 8.5, 9.5);
Cube20Mesh = new THREE.Mesh(CubeGeometry20);
Cube40Mesh = new THREE.Mesh(CubeGeometry40);
CubesLoaded = true;
};
function LoadCubeMaterial()
{
CubeMaterials[0] = new THREE.MeshBasicMaterial({color:0xff0000 });
cMaterialCount++;
CubeMaterials[1] = new THREE.MeshBasicMaterial({color:0xffff00 });
cMaterialCount++;
CubeMaterials[2] = new THREE.MeshBasicMaterial({color:0xffffff });
cMaterialCount++;
CubeMaterials[3] = new THREE.MeshBasicMaterial({color:0x0000ff });
cMaterialCount++;
CubeMaterials[4] = new THREE.MeshBasicMaterial({color:0x00ffff });
cMaterialCount++;
CubeMaterials[5] = new THREE.MeshBasicMaterial({color:0x772255 });
cMaterialCount++;
CubeMaterials[6] = new THREE.MeshBasicMaterial({color:0x552277 });
cMaterialCount++;
CubeMaterials[7] = new THREE.MeshBasicMaterial({color:0x222299 });
cMaterialCount++;
CubeMaterials[8] = new THREE.MeshBasicMaterial({color:0x992222 });
cMaterialCount++;
CubeMaterials[9] = new THREE.MeshBasicMaterial({color:0x000000 });
cMaterialCount++;
};
function DisplayCubes(scene1)
{
if(CubeGeometryTier1.faces.length > 0)
{
var material = new THREE.MeshNormalMaterial();
// save the unsorted geometry.
CubeGeometryArray.push(CubeGeometryTier1.clone());
CubeGeometryTier1.sortFacesByMaterialIndex();
var Cubemesh = new THREE.Mesh(CubeGeometryTier1, new THREE.MeshFaceMaterial(CubeMaterials));
Cubemesh.matrixAutoUpdate = false;
Cubemesh.updateMatrix();
Cubemesh.name = "Tier1";
scene1.add(Cubemesh);
}
if(CubeGeometryTier2.faces.length > 0)
{
// save the unsorted geometry.
CubeGeometryArray.push(CubeGeometryTier2.clone());
// sorting is a HUGE performance boost
CubeGeometryTier2.sortFacesByMaterialIndex();
var Cubemesh = new THREE.Mesh(CubeGeometryTier2, CubeMaterials);
Cubemesh.matrixAutoUpdate = false;
Cubemesh.updateMatrix();
Cubemesh.name = "Tier2";
scene1.add(Cubemesh);
}
if(CubeGeometryTier3.faces.length > 0)
{
CubeGeometryArray.push(CubeGeometryTier3.clone());
CubeGeometryTier3.sortFacesByMaterialIndex();
var Cubemesh = new THREE.Mesh(CubeGeometryTier3, CubeMaterials);
Cubemesh.matrixAutoUpdate = false;
Cubemesh.updateMatrix();
Cubemesh.name = "Tier3";
scene1.add(Cubemesh);
}
if(CubeGeometryTier4.faces.length > 0)
{
CubeGeometryArray.push(CubeGeometryTier4.clone());
CubeGeometryTier4.sortFacesByMaterialIndex();
var Cubemesh = new THREE.Mesh(CubeGeometryTier4, CubeMaterials);
Cubemesh.matrixAutoUpdate = false;
Cubemesh.updateMatrix();
Cubemesh.name = "Tier4";
scene1.add(Cubemesh);
}
if(CubeGeometryTier5.faces.length > 0)
{
CubeGeometryArray.push(CubeGeometryTier5.clone());
CubeGeometryTier5.sortFacesByMaterialIndex();
var Cubemesh = new THREE.Mesh(CubeGeometryTier5, CubeMaterials);
Cubemesh.matrixAutoUpdate = false;
Cubemesh.updateMatrix();
Cubemesh.name = "Tier5";
scene1.add(Cubemesh);
}
};
// merging geometry for improved performance.
function AddCubeGeometry(geom, matrix,tier)
{
switch(tier)
{
case 1:
//CubeGeometryTier1.merge(geom, matrix, tier - 1);
CubeGeometryTier1.merge(geom, matrix);
break;
case 2:
CubeGeometryTier2.merge(geom, matrix,tier - 1);
break;
case 3:
CubeGeometryTier3.merge(geom, matrix,tier - 1);
break;
case 4:
CubeGeometryTier4.merge(geom, matrix,tier - 1);
break;
case 5:
CubeGeometryTier5.merge(geom, matrix,tier - 1);
break;
// forty footers
case 6:
// CubeGeometryTier1.merge(geom, matrix,tier - 1);
CubeGeometryTier1.merge(geom, matrix);
break;
case 7:
CubeGeometryTier2.merge(geom, matrix,tier - 1);
break;
case 8:
CubeGeometryTier3.merge(geom, matrix,tier - 1);
break;
case 9:
CubeGeometryTier4.merge(geom, matrix,tier - 1);
break;
case 10:
CubeGeometryTier5.merge(geom, matrix,tier - 1);
break;
default:
CubeGeometryTier1.merge(geom, matrix,0);
break;
}
};
motioncontrol.position.y = 10;
motioncontrol.position.x = -50;
motioncontrol.position.z = 0;
motioncontrol.rotation.x = 0;
motioncontrol.rotation.z = motioncontrol.distancePointZ * Math.cos(0);
motioncontrol.rotation.x = motioncontrol.distancePointZ * Math.sin(0);
function LoadCubes()
{
var cnt = 0;
for(var x = 0; x < 5; x++)
{
addCube20(cnt, 0, x * 23.0, 90, 1, true);
cnt++;
addCube40(cnt, 10, x * 43, 90, 1, true);
cnt++;
}
};
// game systems code
var keyboardControls = (function() {
var keys = { SP : 32, Q:81, E:69, W : 87, A : 65, S : 83, D : 68, UP : 38, LT : 37, DN : 40, RT : 39 };
var keysPressed = {};
(function( watchedKeyCodes ) {
var handler = function( down ) {
return function( e ) {
var index = watchedKeyCodes.indexOf( e.keyCode );
if( index >= 0 ) {
keysPressed[watchedKeyCodes[index]] = down; e.preventDefault();
}
};
};
window.addEventListener( "keydown", handler( true ), false );
window.addEventListener( "keyup", handler( false ), false );
})([
keys.SP, keys.Q, keys.E, keys.W, keys.A, keys.S, keys.D, keys.UP, keys.LT, keys.DN, keys.RT
]);
return function() {
// look around
if (keysPressed[keys.Q])
{
motioncontrol.tiltangle += motioncontrol.bumpdegrees;
if (motioncontrol.tiltangle < 0)
motioncontrol.tiltangle += 360;
var angle = (Math.PI / 180.0) * motioncontrol.tiltangle;
motioncontrol.rotation.y = motioncontrol.distancePointZ * Math.sin(angle);
}
if (keysPressed[keys.E])
{
motioncontrol.tiltangle -= motioncontrol.bumpdegrees;
if (motioncontrol.tiltangle < 0)
motioncontrol.tiltangle += 360;
var angle = (Math.PI / 180.0) * motioncontrol.tiltangle;
motioncontrol.rotation.y = motioncontrol.distancePointZ * Math.sin(angle);
}
if (keysPressed[keys.W])
{
motioncontrol.position.y += motioncontrol.bumpposition;
if (motioncontrol.position.y > 1000.0)
motioncontrol.position.y = 1000.0;
}
if (keysPressed[keys.S])
{
motioncontrol.position.y += -motioncontrol.bumpposition;
if (motioncontrol.position.y < 1.0)
motioncontrol.position.y = 1;
}
if(keysPressed[keys.A])
{
var angle = Math.atan2(motioncontrol.rotation.x, motioncontrol.rotation.z);
angle += Math.PI / 180.0 * 90;
var message = "Angle " + angle * 180/Math.PI;
motioncontrol.position.z += motioncontrol.bumpposition * Math.cos(angle);
motioncontrol.position.x += motioncontrol.bumpposition * Math.sin(angle);
}
if(keysPressed[keys.D])
{
var angle = Math.atan2(motioncontrol.rotation.x, motioncontrol.rotation.z);
angle += Math.PI / 180.0 * -90;
var message = "Angle " + angle * 180/Math.PI;
motioncontrol.position.z += motioncontrol.bumpposition * Math.cos(angle);
motioncontrol.position.x += motioncontrol.bumpposition * Math.sin(angle);
}
// forward
if (keysPressed[keys.UP])
{
var angle = Math.atan2(motioncontrol.rotation.x, motioncontrol.rotation.z);
var message = "Angle " + angle * 180/Math.PI;
motioncontrol.position.z += motioncontrol.bumpposition * Math.cos(angle);
motioncontrol.position.x += motioncontrol.bumpposition * Math.sin(angle);
}
// backward
if(keysPressed[keys.DN])
{
var deltaX = motioncontrol.rotation.z - motioncontrol.position.z;
var deltaY = motioncontrol.rotation.x - motioncontrol.position.x;
// var angle = Math.atan2(deltaY, deltaX);
var angle = Math.atan2(motioncontrol.rotation.x, motioncontrol.rotation.z);
angle += Math.PI;
var message = "Angle " + angle * 180/Math.PI;
motioncontrol.position.z += motioncontrol.bumpposition * Math.cos(angle);
motioncontrol.position.x += motioncontrol.bumpposition * Math.sin(angle);
}
if(keysPressed[keys.LT])
{
motioncontrol.rotationanglezx -= motioncontrol.bumpdegrees;
if (motioncontrol.rotationanglezx < 0)
motioncontrol.rotationanglezx += 360;
var angle = (Math.PI / 180.0) * motioncontrol.rotationanglezx;
motioncontrol.rotation.x = motioncontrol.distancePointZ * Math.cos(angle) - motioncontrol.distancePointZ * Math.sin(angle);
motioncontrol.rotation.z = motioncontrol.distancePointZ * Math.sin(angle) + motioncontrol.distancePointZ * Math.cos(angle);
}
if(keysPressed[keys.RT])
{
motioncontrol.rotationanglezx += motioncontrol.bumpdegrees;
if (motioncontrol.rotationanglezx > 360)
motioncontrol.rotationanglezx -= 360;
var angle = (Math.PI / 180.0) * motioncontrol.rotationanglezx;
motioncontrol.rotation.x = motioncontrol.distancePointZ * Math.cos(angle) - motioncontrol.distancePointZ * Math.sin(angle);
motioncontrol.rotation.z = motioncontrol.distancePointZ * Math.sin(angle) + motioncontrol.distancePointZ * Math.cos(angle);
}
};
})();
var updateCamera = (function() {
return function() {
camera.position.copy(motioncontrol.position);
camera.lookAt(motioncontrol.rotation);
var message = "Rotation " + motioncontrol.rotationanglezx + "<BR>";
message += "X " + motioncontrol.position.x + "<BR>";
message += "Y " + motioncontrol.position.y + "<BR>";
message += "Z " + motioncontrol.position.z + "<BR>";
message += "X " + motioncontrol.rotation.x + "<BR>";
message += "Y " + motioncontrol.rotation.y + "<BR>";
message += "Z " + motioncontrol.rotation.z + "<BR>";
var angle = Math.atan2(motioncontrol.rotation.x, motioncontrol.rotation.z);
message += "Angle " + angle * 180 / Math.PI + "<BR>";
message += "Use Arrows w,s,e,d,q,a to navigate<BR>";
writeToScreen2(message);
};
})();
function render() {
if(cMaterialCount > 9 && cMaterialCount < 200 && CubesLoaded)
{
cMaterialCount = 200;
LoadCubes();
DisplayCubes(scene);
}
keyboardControls();
updateCamera();
renderer.render( scene, camera );
requestAnimationFrame( render );
};
render();
</script>
</body>
</html>
TLDR Array Mutation and Static Offsets are a dangerous mix
First, I recommend you post fiddles of some sort of your code. I made one here of your example. Second, you could really use some DRYing to shorten and clarify your code. Third, in code of this size, I recommend separating and grouping your code somehow (files, tasks, even comment blocks). Last, in a demo like this, I see no reason to roll your own controls. Check out Orbit Camera or the like that THREE.js offers.
Anyway, I gathered you collect a large number of cubes into 10 'tiers' of THREE.Geometry for rendering purposes. Then on click (~line 180), raycast out, and try to remove just that cube from the geometry. Here's your relevant code:
intersects = raycaster.intersectObjects(CubeArray);
if(intersects.length > 0)
{
if(intersects[0].object.name != null)
{
var offset = intersects[0].object.name * 8;
var offsetfaces = intersects[0].object.name * 12;
var index = intersects[0].object.name;
var selectedObject = scene.getObjectByName("Tier1");
scene.remove(selectedObject);
CubeArray.splice(index, 1);
CubeGeometryTier1 = CubeGeometryArray[0];
CubeGeometryTier1.vertices.splice(offset, 8);
CubeGeometryTier1.faces.splice(offsetfaces, 12);
CubeGeometryTier1.faceVertexUvs[0].splice(offsetfaces, 12);
CubeGeometryArray[0] = CubeGeometryTier1.clone();
CubeGeometryTier1.sortFacesByMaterialIndex();
var cmesh = new THREE.Mesh(CubeGeometryTier1, new THREE.MeshFaceMaterial(CubeMaterials));
cmesh.matrixAutoUpdate = false;
cmesh.updateMatrix();
cmesh.name = "Tier1";
scene.add(cmesh);
}
else
INTERSECTED = null;
}
Here's how I read this snippet:
Cast out against the CubeArray
Without a hit something or if it lacks a name, return
Implict cast some string names (!) to numbers to compute location in Tier
Remove an object from the scene by the name "Tier1", regardless of any other input
Set CubeGeometryTier1 to the first index of CubeGeometryArray, regardless of raycast
Fiddle with now overwritten CubeGeometryTier1 to remove geometry
Reassign CubeGeometryArray[0] with the changed object of CubeGeometryTier1
Build a new mesh based on CubeGeometryTier1, call it "Tier1" and dump it back into the scene
I'll admit I haven't entirely traced the hundreds of line where you build your cubes, but this makes little sense to me. Assuming your use of CubeGeometry[Tier|Array] and hard coded names and indices are correct, what really grabs me is the use of static offsets when you mutate the array.
You splice CubeArray to remove that 'ghost' cube from getting picked again, but none of the other 'ghost' cubes changed, notably their offsets names, while the geometry that you rebuilt into Tier1 did. Past the spliced cube, all of them index names will be wrong.
Here's an example in simpler form:
//set up
var baseArray = [0, 1, 2, 3, 4, 5].map(i => '' + i);
const getRandomInt = (min, max) => {
return Math.floor(Math.random() * (max - min)) + min;
};
const pickRandomElementFrombaseArray = () => {
const pickedIndex = getRandomInt(0, baseArray.length);
return baseArray[pickedIndex];
};
// operationally equivilent to splicing your Tiers of their geometry
const yankIndex = (value) => {
//value is a string in this case
const index = +value;
if (index < 0 || index > baseArray.length - 1) {
throw `Unable to remove invalid index ${index}`
} else {
baseArray.splice(index, 1);
}
};
// Run the test until empty or failure
var messages = [`Starting with ${baseArray}`];
while (baseArray.length > 0) {
const pickedValue = pickRandomElementFrombaseArray();
messages.push(`Picked element ${pickedValue} to remove`);
try {
yankIndex(pickedValue);
messages.push(`Now array is ${baseArray}`);
} catch (e) {
messages.push(`ALERT: ${e}`);
break;
}
}
messages.push('Test complete');
const div = $('#div');
messages.map(msg => `<p>${msg}</p>`).forEach(msg => div.append(msg))
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title></title>
</head>
<body>
<h2>Results</h2>
<div id="div"></div>
</body>
</html>
Try it a few times. There's only a 0.8% chance the array will be exhausted before an error.
Sneaky errors can creep into code when you are dealing with mutation of arrays. Ignoring restructuring the code entirely, options that spring to mind:
Maintain an offset map on each removal. Essentially, rebuild each cube's offset on every action. You could do this in the CubeArray or, if creating/mutating 10k heavy THREE.js objects is not to your liking, yet another level of indirection that maps each cube id to an offset in the Tiers
Invisible Objects Never really remove the geometry (ie, don't splice the Tier array), just hide it. Scale the faces to 0, invisible mat, whatever. This means all the offsets you pre-generate will hold. Downsides are invisible geo isn't free, this won't help if you need to change the scene in other ways, and you'll have to scan the other hits from thee.raycast

Key down event to change background canvas

So I'm trying to have the down arrow make the background of my canvas change. I'm having trouble just getting the button press to work itself.
Also I was told that I would have to have a function redraw all the shapes that are already there at the beginning as well, which I am also stuck on what to change.
Here is a JSFiddle of what I have going so far, any suggestions are appreciated!
https://jsfiddle.net/u8avnky2/
var mainCanvas = document.getElementById("canvas");
var mainContext = mainCanvas.getContext('2d');
//rotate canvas
function buttonClick() {
mainContext.rotate(20*Math.PI/180);
}
//key down event
window.addEventListener('keydown', function(event) {
if (event.keyCode === 40) {
fillBackgroundColor();
}
});
function fillBackgroundColor() {
var colors = ["red", "green", "blue", "orange", "purple", "yellow"];
var color = colors[Math.floor(Math.random()*colors.length)];
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
mainContext.fillStyle = color;
mainContext.fillRect(0, 0, canvas.width, canvas.height);
}
function check() {
mainContext.clearRect(square.x,square.y,square.w,square.h);
}
var circles = new Array();
var requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
function Circle(radius, speed, width, xPos, yPos) {
this.radius = radius;
this.speed = speed;
this.width = width;
this.xPos = xPos;
this.yPos = yPos;
this.opacity = .1 + Math.random() * .5;
this.counter = 0;
var signHelper = Math.floor(Math.random() * 2);
if (signHelper == 1) {
this.sign = -1;
} else {
this.sign = 1;
}
}
//drawing circle
Circle.prototype.update = function () {
this.counter += this.sign * this.speed;
mainContext.beginPath();
mainContext.arc(this.xPos + Math.cos(this.counter / 100) * this.radius,
this.yPos + Math.sin(this.counter / 100) * this.radius,
this.width,
0,
Math.PI * 2,
false);
mainContext.closePath();
mainContext.fillStyle = 'rgba(255, 255, 51,' + this.opacity + ')';
mainContext.fill();
};
function setupCircles() {
for (var i = 0; i < 25; i++) {
var randomX = Math.round(-200 + Math.random() * 700);
var randomY = Math.round(-200 + Math.random() * 700);
var speed = .2 + Math.random() * 3;
var size = 5 + Math.random() * 100;
var radius = 5 + Math.random() * 100;
var circle = new Circle(radius, speed, size, randomX, randomY);
circles.push(circle);
}
drawAndUpdate();
}
setupCircles();
function drawAndUpdate() {
mainContext.clearRect(0, 0, 1000, 1000);
for (var i = 0; i < circles.length; i++) {
var myCircle = circles[i];
myCircle.update();
}
requestAnimationFrame(drawAndUpdate);
}
jsFiddle : https://jsfiddle.net/CanvasCode/u8avnky2/1/
I added a variable known as color globally, so the fillBackgroundColor can access that instead.
var color = "white";
Then in your drawAndUpdate function we just do a fillRect with the color variable using the canvas width and height and it works.
function drawAndUpdate() {
mainContext.fillStyle = color;
mainContext.fillRect(0, 0, mainCanvas.width, mainCanvas.height);
for (var i = 0; i < circles.length; i++) {
var myCircle = circles[i];
myCircle.update();
}
requestAnimationFrame(drawAndUpdate);
}

Resources