Get Touch Event Relative To Under-Sized Frame Buffer Object - javascript

I coded a fluid simulation using shaders (I used THREE.js) for my webpage. I wanted it to run fast even on mobile, so I decided that I was going to simulate on a lower resolution (4 times smaller than the Render Target), I managed to get it working for Mouse Events, but I haven't been able to decipher how to properly scale Touch Events so that they matche real touch positions.
function handleMove(evt) {
evt.preventDefault();
var touches = evt.targetTouches;
var x = 0, y = 0;
if (BufferBUniforms.iMouse.value.z === 1) {
var element = document.getElementById("container").getBoundingClientRect();
var bodyRect = document.body.getBoundingClientRect();
var h = (element.top - bodyRect.top);
var w = (element.left - bodyRect.left);
// One way I tried.
x = ( touches[0].pageX - w ) / scaleMax;
y = height - ( touches[0].pageY - h ) / scaleMax;
// Another way I tried.
x = ( touches[0].pageX - w ) / scaleMax;
y = height - ( touches[0].pageY - h ) / scaleMaxO;
BufferAUniforms.iMouse.value.x = x;
BufferAUniforms.iMouse.value.y = y;
}
}
This is a snippet where I defined some of the variables mentioned above:
scale = window.devicePixelRatio;
renderer.setPixelRatio(scale);
container.appendChild(renderer.domElement);
height = window.innerHeight * 0.25;
height = THREE.Math.floorPowerOfTwo( height )
scaleMax = window.innerHeight / height;
width = window.innerWidth * 0.25;
width = THREE.Math.floorPowerOfTwo(width)
scaleRatio = width / height;
scaleMaxO = window.innerWidth / width;
renderer.setSize(width * scaleMax, height * scaleMaxO);
The thing is that it works when using Chrome Dev Tools on Mobile Emulator, but not when using a Samsung S9 Plus.
You can see the whole thing here

Assuming you're drawing that smaller render target to fill the canvas it should just be
const rect = renderer.domElement.getBoundingClientRect();
const x = (touches[0].clientX - rect.left) * renderTargetWidth / rect.width;
const y = (touches[0].clientY - rect.top ) * renderTargetHeight / rect.height;
Now X and Y are in pixels in the render target though you might want to flip Y
const y = renderTargetHeight -
(touches[0].clientY - rect.top) * renderTargetHeight / rect.height;
Note the mouse is no different. In fact this should work
function computeRenderTargetRelativePosition(e) {
const rect = renderer.domElement.getBoundingClientRect();
const x = (e.clientX - rect.left) * renderTargetWidth / rect.width;
const y = (e.clientY - rect.top ) * renderTargetHeight / rect.height;
return {x, y};
}
renderer.domElement.addEventListener('mousemove', (e) => {
const pos = computeRenderTargetRelativePosition(e);
... do something with pos...
});
renderer.domElement.addEventListener('touchmove', (e) => {
const pos = computeRenderTargetRelativePosition(e.touches[0]);
... do something with pos...
});
The only complication is if you apply CSS transforms then you need different code.
'use strict';
/* global THREE */
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const rtWidth = 24;
const rtHeight = 12;
const renderTarget = new THREE.WebGLRenderTarget(rtWidth, rtHeight);
const rtCamera = new THREE.OrthographicCamera(0, rtWidth, rtHeight, 0, -1, 1);
const rtScene = new THREE.Scene();
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
const rtMaterial = new THREE.MeshBasicMaterial({color: 'red'});
const rtCube = new THREE.Mesh(geometry, rtMaterial);
rtScene.add(rtCube);
const camera = new THREE.Camera();
const scene = new THREE.Scene();
const planeGeo = new THREE.PlaneBufferGeometry(2, 2);
const material = new THREE.MeshBasicMaterial({
//color: 'blue',
map: renderTarget.texture,
});
const plane = new THREE.Mesh(planeGeo, material);
scene.add(plane);
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
}
// draw render target scene to render target
renderer.setRenderTarget(renderTarget);
renderer.render(rtScene, rtCamera);
renderer.setRenderTarget(null);
// render the scene to the canvas
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function setPos(e) {
const rect = renderer.domElement.getBoundingClientRect();
const x = (e.clientX - rect.left) * rtWidth / rect.width;
const y = rtHeight - (e.clientY - rect.top ) * rtHeight / rect.height;
rtCube.position.set(x, y, 0);
}
renderer.domElement.addEventListener('mousemove', (e) => {
setPos(e);
});
renderer.domElement.addEventListener('touchmove', (e) => {
e.preventDefault();
setPos(e.touches[0]);
}, {passive: false});
}
main();
body {
margin: 0;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
<canvas id="c"></canvas>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r105/three.min.js"></script>

Related

Javascript pseudo-zoom around mouse position in canvas

I would like to "zoom" (mouse wheel) a grid I have, around the mouse position in canvas (like Desmos does).
By zooming, I mean redrawing the lines inside the canvas to look like a zoom effect, NOT performing an actual zoom.
And I would like to only use vanilla javascript and no libraries (so that I can learn).
At this point, I set up a very basic magnification effect that only multiplies the distance between the gridlines, based on the mouse wheel values.
////////////////////////////////////////////////////////////////////////////////
// User contants
const canvasWidth = 400;
const canvasHeight = 200;
const canvasBackground = '#282c34';
const gridCellColor = "#777";
const gridBlockColor = "#505050";
const axisColor = "white";
// Internal constants
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d', { alpha: false });
const bodyToCanvas = 8;
////////////////////////////////////////////////////////////////////////////////
// User variables
let cellSize = 10;
let cellBlock = 5;
let xSubdivs = 40
let ySubdivs = 20
// Internal variables
let grid = '';
let zoom = 0;
let xAxisOffset = xSubdivs/2;
let yAxisOffset = ySubdivs/2;
let mousePosX = 0;
let mousePosY = 0;
////////////////////////////////////////////////////////////////////////////////
// Classes
class Grid{
constructor() {
this.width = canvasWidth,
this.height = canvasHeight,
this.cellSize = cellSize,
this.cellBlock = cellBlock,
this.xSubdivs = xSubdivs,
this.ySubdivs = ySubdivs
}
draw(){
// Show canvas
context.fillStyle = canvasBackground;
context.fillRect(-this.width/2, -this.height/2, this.width, this.height);
// Horizontal lines
this.xSubdivs = Math.floor(this.height / this.cellSize);
for (let i = 0; i <= this.xSubdivs; i++) {this.setHorizontalLines(i);}
// Vertical lines
this.ySubdivs = Math.floor(this.width / this.cellSize);
for (let i = 0; i <= this.ySubdivs; i++) {this.setVerticalLines(i) ;}
// Axis
this.setAxis();
}
setHorizontalLines(i) {
// Style
context.lineWidth = 0.5;
if (i % this.cellBlock == 0) {
// light lines
context.strokeStyle = gridCellColor;
}
else{
// Dark lines
context.strokeStyle = gridBlockColor;
}
//Draw lines
context.beginPath();
context.moveTo(-this.width/2, (this.cellSize * i) - this.height/2);
context.lineTo( this.width/2, (this.cellSize * i) - this.height/2);
context.stroke();
context.closePath();
}
setVerticalLines(i) {
// Style
context.lineWidth = 0.5;
if (i % cellBlock == 0) {
// Light lines
context.strokeStyle = gridCellColor;
}
else {
// Dark lines
context.strokeStyle = gridBlockColor;
}
//Draw lines
context.beginPath();
context.moveTo((this.cellSize * i) - this.width/2, -this.height/2);
context.lineTo((this.cellSize * i) - this.width/2, this.height/2);
context.stroke();
context.closePath();
}
// Axis are separated from the line loops so that they remain on
// top of them (cosmetic measure)
setAxis(){
// Style x Axis
context.lineWidth = 1.5;
context.strokeStyle = axisColor;
// Draw x Axis
context.beginPath();
context.moveTo(-this.width/2, (this.cellSize * yAxisOffset) - this.height/2);
context.lineTo( this.width/2, (this.cellSize * yAxisOffset) - this.height/2);
context.stroke();
context.closePath();
// Style y axis
context.lineWidth = 1.5;
context.strokeStyle = axisColor;
// Draw y axis
context.beginPath();
context.moveTo((this.cellSize * xAxisOffset ) - this.width/2, -this.height/2);
context.lineTo((this.cellSize * xAxisOffset ) - this.width/2, this.height/2);
context.stroke();
context.closePath();
}
}
////////////////////////////////////////////////////////////////////////////////
// Functions
function init() {
// Set up canvas
if (window.devicePixelRatio > 1) {
canvas.width = canvasWidth * window.devicePixelRatio;
canvas.height = canvasHeight * window.devicePixelRatio;
context.scale(window.devicePixelRatio, window.devicePixelRatio);
}
else {
canvas.width = canvasWidth;
canvas.height = canvasHeight;
}
canvas.style.width = canvasWidth + "px";
canvas.style.height = canvasHeight + "px";
// Initialize coordinates in the middle of the canvas
context.translate(canvasWidth/2,canvasHeight/2)
// Setup the grid
grid = new Grid();
// Display the grid
grid.draw();
}
function setZoom(){
grid.cellSize = grid.cellSize + zoom;
grid.draw();
}
////////////////////////////////////////////////////////////////////////////////
//Launch the page
init();
////////////////////////////////////////////////////////////////////////////////
// Update the page on resize
window.addEventListener("resize", init);
// Zoom the canvas with mouse wheel
canvas.addEventListener('mousewheel', function (e) {
e.preventDefault();
e.stopPropagation();
zoom = e.wheelDelta/120;
requestAnimationFrame(setZoom);
})
// Get mouse coordinates on mouse move.
canvas.addEventListener('mousemove', function (e) {
e.preventDefault();
e.stopPropagation();
mousePosX = parseInt(e.clientX)-bodyToCanvas ;
mousePosY = parseInt(e.clientY)-bodyToCanvas ;
})
////////////////////////////////////////////////////////////////////////////////
html, body
{
background:#21252b;
width:100%;
height:100%;
margin:0px;
padding:0px;
overflow: hidden;
}
span{
color:white;
font-family: arial;
font-size: 12px;
margin:8px;
}
#canvas{
margin:8px;
border: 1px solid white;
}
<span>Use mouse wheel to zoom</span>
<canvas id="canvas"></canvas>
This works fine but, as expected, it magnifies the grid from the top left corner not from the mouse position.
So, I thought about detecting the mouse position and then modifying all the "moveTo" and "lineTo" parts. The goal would be to offset the magnified grid so that everything is displaced except the 2 lines intersecting the current mouse coordinates.
For instance, it feels to me that instead of this:
context.moveTo(
(this.cellSize * i) - this.width/2,
-this.height/2
);
I should have something like this:
context.moveTo(
(this.cellSize * i) - this.width/2 + OFFSET BASED ON MOUSE COORDS,
-this.height/2
);
But after hours of trials and errors, I'm stuck. So, any help would be appreciated.
FYI: I already coded a functional panning system that took me days to achieve (that I stripped from the code here for clarity), so I would like to keep most of the logic I have so far, if possible.
Unless my code is total nonsense or has performance issues, of course.
Thank you.
You're already tracking the mouse position in pixels. If you transform the mouse position to the coordinate system of your grid, you can define which grid cell it's hovering.
After zooming in, you can again calculate the mouse's coordinate. When zooming in around another center point, you'll see the coordinate shift.
You can undo that shift by translating the grid's center in the opposite direction.
Here's the main part of it:
function setZoom() {
// Calculate the mouse position before applying the zoom
// in the coordinate system of the grid
const x1 = (mousePosX - grid.centerX) / grid.cellSize;
const y1 = (mousePosY - grid.centerY) / grid.cellSize;
// Make the zoom happen: update the cellSize
grid.cellSize = grid.cellSize + zoom;
// After zoom, you'll see the coordinates changed
const x2 = (mousePosX - grid.centerX) / grid.cellSize;
const y2 = (mousePosY - grid.centerY) / grid.cellSize;
// Calculate the shift
const dx = x2 - x1;
const dy = y2 - y1;
// "Undo" the shift by shifting the coordinate system's center
grid.centerX += dx * grid.cellSize;
grid.centerY += dy * grid.cellSize;
grid.draw();
}
To make this easier to work with, I introduced a grid.centerX and .centerY. I updated your draw method to take the center in to account (and kind of butchered it along the way, but I think you'll manage to improve that a bit).
Here's the complete example:
////////////////////////////////////////////////////////////////////////////////
// User contants
const canvasWidth = 400;
const canvasHeight = 200;
const canvasBackground = '#282c34';
const gridCellColor = "#777";
const gridBlockColor = "#505050";
const axisColor = "white";
// Internal constants
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d', { alpha: false });
const bodyToCanvas = 8;
////////////////////////////////////////////////////////////////////////////////
// User variables
let cellSize = 10;
let cellBlock = 5;
let xSubdivs = 40
let ySubdivs = 20
// Internal variables
let grid = '';
let zoom = 0;
let xAxisOffset = xSubdivs/2;
let yAxisOffset = ySubdivs/2;
let mousePosX = 0;
let mousePosY = 0;
////////////////////////////////////////////////////////////////////////////////
// Classes
class Grid{
constructor() {
this.width = canvasWidth,
this.height = canvasHeight,
this.cellSize = cellSize,
this.cellBlock = cellBlock,
this.xSubdivs = xSubdivs,
this.ySubdivs = ySubdivs,
this.centerX = canvasWidth / 2,
this.centerY = canvasHeight / 2
}
draw(){
// Show canvas
context.fillStyle = canvasBackground;
context.fillRect(0, 0, this.width, this.height);
// Horizontal lines
const minIY = -Math.ceil(this.centerY / this.cellSize);
const maxIY = Math.ceil((this.height - this.centerY) / this.cellSize);
for (let i = minIY; i <= maxIY; i++) {this.setHorizontalLines(i);}
// Vertical lines
const minIX = -Math.ceil(this.centerX / this.cellSize);
const maxIX = Math.ceil((this.width - this.centerX) / this.cellSize);
for (let i = minIX; i <= maxIX; i++) {this.setVerticalLines(i) ;}
this.setVerticalLines(0);
this.setHorizontalLines(0);
}
setLineStyle(i) {
if (i === 0) {
context.lineWidth = 1.5;
context.strokeStyle = axisColor;
} else if (i % cellBlock == 0) {
// Light lines
context.lineWidth = 0.5;
context.strokeStyle = gridCellColor;
} else {
// Dark lines
context.lineWidth = 0.5;
context.strokeStyle = gridBlockColor;
}
}
setHorizontalLines(i) {
// Style
this.setLineStyle(i);
//Draw lines
const y = this.centerY + this.cellSize * i;
context.beginPath();
context.moveTo(0, y);
context.lineTo(this.width, y);
context.stroke();
context.closePath();
}
setVerticalLines(i) {
// Style
this.setLineStyle(i);
//Draw lines
const x = this.centerX + this.cellSize * i;
context.beginPath();
context.moveTo(x, 0);
context.lineTo(x, this.height);
context.stroke();
context.closePath();
}
}
////////////////////////////////////////////////////////////////////////////////
// Functions
function init() {
// Set up canvas
if (window.devicePixelRatio > 1) {
canvas.width = canvasWidth * window.devicePixelRatio;
canvas.height = canvasHeight * window.devicePixelRatio;
context.scale(window.devicePixelRatio, window.devicePixelRatio);
}
else {
canvas.width = canvasWidth;
canvas.height = canvasHeight;
}
canvas.style.width = canvasWidth + "px";
canvas.style.height = canvasHeight + "px";
// Setup the grid
grid = new Grid();
// Display the grid
grid.draw();
}
function setZoom() {
// Calculate the mouse position before applying the zoom
// in the coordinate system of the grid
const x1 = (mousePosX - grid.centerX) / grid.cellSize;
const y1 = (mousePosY - grid.centerY) / grid.cellSize;
grid.cellSize = grid.cellSize + zoom;
// After zoom, you'll see the coordinates changed
const x2 = (mousePosX - grid.centerX) / grid.cellSize;
const y2 = (mousePosY - grid.centerY) / grid.cellSize;
// Calculate the shift
const dx = x2 - x1;
const dy = y2 - y1;
// "Undo" the shift by shifting the coordinate system's center
grid.centerX += dx * grid.cellSize;
grid.centerY += dy * grid.cellSize;
grid.draw();
}
////////////////////////////////////////////////////////////////////////////////
//Launch the page
init();
////////////////////////////////////////////////////////////////////////////////
// Update the page on resize
window.addEventListener("resize", init);
// Zoom the canvas with mouse wheel
canvas.addEventListener('mousewheel', function (e) {
e.preventDefault();
e.stopPropagation();
zoom = e.wheelDelta/120;
requestAnimationFrame(setZoom);
})
// Get mouse coordinates on mouse move.
canvas.addEventListener('mousemove', function (e) {
e.preventDefault();
e.stopPropagation();
mousePosX = parseInt(e.clientX)-bodyToCanvas ;
mousePosY = parseInt(e.clientY)-bodyToCanvas ;
})
////////////////////////////////////////////////////////////////////////////////
html, body
{
background:#21252b;
width:100%;
height:100%;
margin:0px;
padding:0px;
overflow: hidden;
}
span{
color:white;
font-family: arial;
font-size: 12px;
margin:8px;
}
#canvas{
margin:8px;
border: 1px solid white;
}
<canvas id="canvas"></canvas>

Inertia compensated mouse position in Javascript

I am attempting to add an inertia type effect to my parallax background which is based on the current mouse position.
I have found a script that can take the position of the mouse, and make another object follow it with a set inertia within a fixed frame. However I would like to adapt this script so that I can create a function that I instead pass the current mouse x/y position and desired inertia, and it outputs the inertia compensated mouse position which I can then go on to pass to my parallax effect script. My code is below:
Parallax Script:
(function() {
// Add event listener
document.addEventListener("mousemove", parallax);
const elem = document.querySelector("#text");
// Magic happens here
function parallax(e) {
let _w = window.innerWidth/2;
let _h = window.innerHeight/2;
let _mouseX = e.clientX;
let _mouseY = e.clientY;
let _depth1 = `${50 - (_mouseX - _w) * 0.01}% ${50 - (_mouseY - _h) * 0.01}%`;
let _depth2 = `${50 - (_mouseX - _w) * 0.02}% ${50 - (_mouseY - _h) * 0.02}%`;
let _depth3 = `${50 - (_mouseX - _w) * 0.06}% ${50 - (_mouseY - _h) * 0.06}%`;
let x = `${_depth3}, ${_depth2}, ${_depth1}`;
console.log(x);
elem.style.backgroundPosition = x;
}
})();
Inertia Script:
class MouseTracking{
constructor(containerSelector, followerSelector, inertia = 10){
this.$container = document.querySelector(containerSelector);
this.$follower = document.querySelector(followerSelector);
this.inertia = inertia > 0 ? inertia : 1;
console.log(this.$container);
console.log(this.$follower);
this.getDims();
this.xPos = this.maxW/2;
this.yPos = this.maxH/2;
this.mouseX = this.maxW/2;
this.mouseY = this.maxH/2;
this.bindEvents();
this.update();
}
getDims(){
this.maxW = this.$container.clientWidth;
this.maxH = this.$container.clientHeight;
this.elemWidth = this.$follower.getBoundingClientRect().width;
this.elemHeight = this.$follower.getBoundingClientRect().height;
}
onMouse(e){
this.mouseX = e.clientX;
   this.mouseY = e.clientY;
}
bindEvents(){
window.onresize = ()=>{this.getDims()};
this.$container.addEventListener('mousemove', this.onMouse.bind(this));
}
update(){
let dX = this.mouseX - this.xPos - this.elemWidth/2;
let dY = this.mouseY - this.yPos - this.elemHeight/2;
this.xPos += (dX / this.inertia);
this.yPos += (dY / this.inertia);
this.xPrct = (100*this.xPos)/this.maxW;
this.yPrct = (100*this.yPos)/this.maxH;
this.$follower.style.transform = "translate3D("+this.xPos+"px, "+this.yPos+"px, 0)";
requestAnimationFrame(this.update.bind(this));
}
}
new MouseTracking('.background', '#cutout-text', 100);
Codepen for context here

Enable to scale a ImageData from a CanvasRenderingContext2D into a other one

Here I have my main canvas 2D context that is on my screen named baseCtx that is in the custom service drawingService.
zoomCtx is the CanvasRenderingContext2D of my second canvas and zoomCanvas is the HTMLCanvasElement of my secondary canvas.
I want to take a rectangle that is the size of a smaller canvas that have for center my mouse.
CurrentX and CurrentY are the current mousePosition on my main canvas.
That is working just fine I can see the content of the rectangle zone on the secondary canvas.
The problem come when I try to zoom in the secondary canvas that will play the role of a magnifying glass. I can see the zone around my cursor drawing on the secondary canvas, it is just not zoom in.
I just won't zoom event tho I am using the scale method.
Here is my code that is called each time the mouse move
calculateZoomedPixel() {
let sx = this.currentX - this.zoomCanvas.width / 2;
let sy = this.currentY - this.zoomCanvas.height / 2;
const zoom = 2;
let image = this.drawingService.baseCtx.getImageData(sx, sy, this.zoomCanvas.width, this.zoomCanvas.height);
this.zoomCtx.putImageData(image, 0, 0);
this.zoomCtx.translate(zoom * this.zoomCanvas.width, zoom * this.zoomCanvas.height);
this.zoomCtx.scale(zoom, zoom);
this.zoomCtx.drawImage(this.zoomCanvas, 0, 0);
this.zoomCtx.translate(-zoom * this.zoomCanvas.width, -zoom * this.zoomCanvas.height);
}
Here is an example of what i want the second canvas to look like:
And here is an example of my application (The smaller canvas to the left should be zoom in):
A few problems here:
Your translate calls are not made properly, you are only translating back after you drew the image, which has no good effect.
You never reset the transformation matrix after you set its scale, so at second call, your zoom canvas as its scale set to 4, at third, to 8 etc.
To workaround both these problems, you can use the absolute setTransform() method to set both the scale and the translate in one call, and then translate back using the drawImage's parameters:
const source_width = source.width = 500;
const source_height = source.height = 500;
const mag_width = magnifier.width = 100;
const mag_height = magnifier.height = 100;
const source_ctx = source.getContext("2d");
source_ctx.fillStyle = "white";
source_ctx.fillRect(0, 0, source_width, source_height);
source_ctx.stroke(randomLines());
const mag_ctx = magnifier.getContext("2d");
mag_ctx.imageSmoothingEnabled = false;
mag_ctx.globalCompositeOperation = "copy";
source.onmousemove = (evt) => {
const rect = source.getBoundingClientRect();
const current_x = evt.clientX - rect.left;
const current_y = evt.clientY - rect.top;
const sx = current_x - mag_width / 2;
const sy = current_y - mag_height / 2;
const zoom = 10;
const image = source_ctx.getImageData(sx, sy, mag_width, mag_height);
mag_ctx.putImageData(image, 0, 0);
// set the zoom and translate to center in one call
mag_ctx.setTransform(zoom, 0, 0, zoom, mag_width/2, mag_height/2 );
// translate back in drawImage to center our image
mag_ctx.drawImage(magnifier, -mag_width/2, -mag_height/2);
// reset to identity matrix
mag_ctx.setTransform(1, 0, 0, 1, 0, 0);
};
function randomLines() {
const path = new Path2D();
for( let i = 0; i<30; i++ ) {
const x = Math.random() * source_width;
const y = Math.random() * source_height;
path.lineTo(x, y);
}
return path;
}
#magnifier { position: absolute; border: 1px solid; }
<canvas id="magnifier"></canvas>
<canvas id="source"></canvas>
But you should not call getImageData in a mousemove event like this. getImageData is utterly slow as it requires to move the context's buffer from the GPU to the CPU. Call it only once to grab the full canvas and reuse only that single ImageData:
const source_width = source.width = 500;
const source_height = source.height = 500;
const mag_width = magnifier.width = 100;
const mag_height = magnifier.height = 100;
const source_ctx = source.getContext("2d");
source_ctx.fillStyle = "white";
source_ctx.fillRect(0, 0, source_width, source_height);
source_ctx.stroke(randomLines());
const mag_ctx = magnifier.getContext("2d");
mag_ctx.imageSmoothingEnabled = false;
// get the image data only once, when the drawing is done
const image = source_ctx.getImageData(0, 0, source_width, source_height );
source.onmousemove = (evt) => {
mag_ctx.clearRect(0, 0, mag_width, mag_height);
const rect = source.getBoundingClientRect();
const current_x = evt.clientX - rect.left;
const current_y = evt.clientY - rect.top;
const sx = current_x - mag_width / 2;
const sy = current_y - mag_height / 2;
const zoom = 10;
// draw the correct portion of the ImageData
mag_ctx.putImageData(image, -sx, -sy, sx, sy, source_width, source_height);
// set the zoom and translate to center in one call
mag_ctx.setTransform(zoom, 0, 0, zoom, mag_width/2, mag_height/2 );
// translate back in drawImage to center our image
mag_ctx.drawImage(magnifier, -mag_width/2, -mag_height/2);
// reset to identity matrix
mag_ctx.setTransform(1, 0, 0, 1, 0, 0);
};
function randomLines() {
const path = new Path2D();
for( let i = 0; i<30; i++ ) {
const x = Math.random() * source_width;
const y = Math.random() * source_height;
path.lineTo(x, y);
}
return path;
}
#magnifier { position: absolute; border: 1px solid; }
<canvas id="magnifier"></canvas>
<canvas id="source"></canvas>
And if you are not doing anything with these pixels' data, then just use drawImage directly:
const source_width = source.width = 500;
const source_height = source.height = 500;
const mag_width = magnifier.width = 100;
const mag_height = magnifier.height = 100;
const source_ctx = source.getContext("2d");
source_ctx.fillStyle = "white";
source_ctx.fillRect(0, 0, source_width, source_height);
source_ctx.stroke(randomLines());
const mag_ctx = magnifier.getContext("2d");
mag_ctx.imageSmoothingEnabled = false;
source.onmousemove = (evt) => {
mag_ctx.clearRect(0, 0, mag_width, mag_height);
const rect = source.getBoundingClientRect();
const current_x = evt.clientX - rect.left;
const current_y = evt.clientY - rect.top;
const sx = current_x - mag_width / 2;
const sy = current_y - mag_height / 2;
const zoom = 10;
mag_ctx.setTransform(zoom, 0, 0, zoom, mag_width/2, mag_height/2 );
// draw source canvas directly
mag_ctx.drawImage(source, sx, sy, mag_width, mag_height, -mag_width/2, -mag_height/2, mag_width, mag_height);
// reset to identity matrix
mag_ctx.setTransform(1, 0, 0, 1, 0, 0);
};
function randomLines() {
const path = new Path2D();
for( let i = 0; i<30; i++ ) {
const x = Math.random() * source_width;
const y = Math.random() * source_height;
path.lineTo(x, y);
}
return path;
}
#magnifier { position: absolute; border: 1px solid; }
<canvas id="magnifier"></canvas>
<canvas id="source"></canvas>

Optimise canvas drawing of a circle

I am new to HTML5 canvas and looking to make a few circles move in random directions for a fancy effect on my website.
I have noticed that when these circles move, the CPU usage is very high. When there is just a couple of circles moving it is often ok, but when there is around 5 or more it starts to be a problem.
Here is a screenshot of profiling this in Safari for a few seconds with 5 circles.
Here is the code I have so far for my Circle component:
export default function Circle({ color = null }) {
useEffect(() => {
if (!color) return
let requestId = null
let canvas = ref.current
let context = canvas.getContext("2d")
let ratio = getPixelRatio(context)
let canvasWidth = getComputedStyle(canvas).getPropertyValue("width").slice(0, -2)
let canvasHeight = getComputedStyle(canvas).getPropertyValue("height").slice(0, -2)
canvas.width = canvasWidth * ratio
canvas.height = canvasHeight * ratio
canvas.style.width = "100%"
canvas.style.height = "100%"
let y = random(0, canvas.height)
let x = random(0, canvas.width)
const height = random(100, canvas.height * 0.6)
let directionX = random(0, 1) === 0 ? "left" : "right"
let directionY = random(0, 1) === 0 ? "up" : "down"
const speedX = 0.1
const speedY = 0.1
context.fillStyle = color
const render = () => {
//draw circle
context.clearRect(0, 0, canvas.width, canvas.height)
context.beginPath()
context.arc(x, y, height, 0, 2 * Math.PI)
//prevent circle from going outside of boundary
if (x < 0) directionX = "right"
if (x > canvas.width) directionX = "left"
if (y < 0) directionY = "down"
if (y > canvas.height) directionY = "up"
//move circle
if (directionX === "left") x -= speedX
else x += speedX
if (directionY === "up") y -= speedY
else y += speedY
//apply color
context.fill()
//animate
requestId = requestAnimationFrame(render)
}
render()
return () => {
cancelAnimationFrame(requestId)
}
}, [color])
let ref = useRef()
return <canvas ref={ref} />
}
Is there a more performant way to draw and move circles using canvas?
When they do not move, the CPU usage starts off around ~3% then drops to less than 1%, and when I remove the circles from the DOM, the CPU usage is always less than 1%.
I understand it's often better to do these types of animations with CSS (as I believe it uses the GPU rather than the CPU), but I couldn't work out how to get it to work using the transition CSS property. I could only get the scale transformation to work.
My fancy effect only looks "cool" when there are many circles moving on the screen, hence looking for a more performant way to draw and move the circles.
Here is a sandbox for a demo: https://codesandbox.io/s/async-meadow-vx822 (view in chrome or safari for best results)
Here is a slightly different approach to combine circles and background to have only one canvas element to improve rendered dom.
This component uses the same colours and sizes with your randomization logic but stores all initial values in a circles array before rendering anything. render functions renders background colour and all circles together and calculates their move in each cycle.
export default function Circles() {
useEffect(() => {
const colorList = {
1: ["#247ba0", "#70c1b3", "#b2dbbf", "#f3ffbd", "#ff1654"],
2: ["#05668d", "#028090", "#00a896", "#02c39a", "#f0f3bd"]
};
const colors = colorList[random(1, Object.keys(colorList).length)];
const primary = colors[random(0, colors.length - 1)];
const circles = [];
let requestId = null;
let canvas = ref.current;
let context = canvas.getContext("2d");
let ratio = getPixelRatio(context);
let canvasWidth = getComputedStyle(canvas)
.getPropertyValue("width")
.slice(0, -2);
let canvasHeight = getComputedStyle(canvas)
.getPropertyValue("height")
.slice(0, -2);
canvas.width = canvasWidth * ratio;
canvas.height = canvasHeight * ratio;
canvas.style.width = "100%";
canvas.style.height = "100%";
[...colors, ...colors].forEach(color => {
let y = random(0, canvas.height);
let x = random(0, canvas.width);
const height = random(100, canvas.height * 0.6);
let directionX = random(0, 1) === 0 ? "left" : "right";
let directionY = random(0, 1) === 0 ? "up" : "down";
circles.push({
color: color,
y: y,
x: x,
height: height,
directionX: directionX,
directionY: directionY
});
});
const render = () => {
context.fillStyle = primary;
context.fillRect(0, 0, canvas.width, canvas.height);
circles.forEach(c => {
const speedX = 0.1;
const speedY = 0.1;
context.fillStyle = c.color;
context.beginPath();
context.arc(c.x, c.y, c.height, 0, 2 * Math.PI);
if (c.x < 0) c.directionX = "right";
if (c.x > canvas.width) c.directionX = "left";
if (c.y < 0) c.directionY = "down";
if (c.y > canvas.height) c.directionY = "up";
if (c.directionX === "left") c.x -= speedX;
else c.x += speedX;
if (c.directionY === "up") c.y -= speedY;
else c.y += speedY;
context.fill();
context.closePath();
});
requestId = requestAnimationFrame(render);
};
render();
return () => {
cancelAnimationFrame(requestId);
};
});
let ref = useRef();
return <canvas ref={ref} />;
}
You can simply replace all bunch of circle elements and background style with this one component in your app component.
export default function App() {
return (
<>
<div className="absolute inset-0 overflow-hidden">
<Circles />
</div>
<div className="backdrop-filter-blur-90 absolute inset-0 bg-gray-900-opacity-20" />
</>
);
}
I tried to assemble your code as possible, it seems you have buffer overflow (blue js heap), you need to investigate here, these are the root cause.
The initial approach is to create circle just once, then animate the child from parent, by this way you avoid intensive memory and CPU computing.
Add how many circles by clicking on the canvas, canvas credit goes to Martin
Update
Following for alexander discussion it is possible to use setTimeout, or Timeinterval (Solution 2)
Soltion #1
App.js
import React from 'react';
import { useCircle } from './useCircle';
import './App.css';
const useAnimationFrame = callback => {
// Use useRef for mutable variables that we want to persist
// without triggering a re-render on their change
const requestRef = React.useRef();
const previousTimeRef = React.useRef();
const animate = time => {
if (previousTimeRef.current != undefined) {
const deltaTime = time - previousTimeRef.current;
callback(deltaTime)
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
}
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []); // Make sure the effect runs only once
}
function App() {
const [count, setCount] = React.useState(0)
const [coordinates, setCoordinates, canvasRef, canvasWidth, canvasHeight, counts] = useCircle();
const speedX = 1 // tunne performance by changing this
const speedY = 1 // tunne performance by changing this
const requestRef = React.useRef();
const previousTimeRef = React.useRef();
const handleCanvasClick = (event) => {
// on each click get current mouse location
const currentCoord = { x: event.clientX, y: event.clientY ,directionX:"right",directionY:"down"};
// add the newest mouse location to an array in state
setCoordinates([...coordinates, currentCoord]);
// query.push(currentCoord)
//query.push(currentCoord)
};
const move = () => {
let q = [...coordinates]
q.map(coordinate => { return { x: coordinate.x + 10, y: coordinate.y + 10 } })
setCoordinates(q)
}
const handleClearCanvas = (event) => {
setCoordinates([]);
};
const animate = time => {
//if (time % 2===0){
setCount(time)
if (previousTimeRef.current != undefined) {
const deltaTime = time - previousTimeRef.current;
setCoordinates(coordinates => coordinates.map((coordinate)=> {
let x=coordinate.x;
let y=coordinate.y;
let directionX=coordinate.directionX
let directionY=coordinate.directionY
if (x < 0) directionX = "right"
if (x > canvasWidth) directionX = "left"
if (y < 0) directionY = "down"
if (y > canvasHeight) directionY = "up"
if (directionX === "left") x -= speedX
else x += speedX
if (directionY === "up") y -= speedY
else y += speedY
return { x:x,y:y,directionX:directionX,directionY:directionX}
}))
// }
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
}
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []); // Make sure the effect runs only once
return (
<main className="App-main" >
<div>{Math.round(count)}</div>
<canvas
className="App-canvas"
ref={canvasRef}
width={canvasWidth}
height={canvasHeight}
onClick={handleCanvasClick}
/>
<div className="button" >
<button onClick={handleClearCanvas} > CLEAR </button>
</div>
</main>
);
};
export default App;
userCircle.js
import React, { useState, useEffect, useRef } from 'react';
var circle = new Path2D();
circle.arc(100, 100, 50, 0, 2 * Math.PI);
const SCALE = 1;
const OFFSET = 80;
export const canvasWidth = window.innerWidth * .5;
export const canvasHeight = window.innerHeight * .5;
export const counts=0;
export function draw(ctx, location) {
console.log("attempting to draw")
ctx.fillStyle = 'red';
ctx.shadowColor = 'blue';
ctx.shadowBlur = 15;
ctx.save();
ctx.scale(SCALE, SCALE);
ctx.translate(location.x / SCALE - OFFSET, location.y / SCALE - OFFSET);
ctx.rotate(225 * Math.PI / 180);
ctx.fill(circle);
ctx.restore();
};
export function useCircle() {
const canvasRef = useRef(null);
const [coordinates, setCoordinates] = useState([]);
useEffect(() => {
const canvasObj = canvasRef.current;
const ctx = canvasObj.getContext('2d');
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
coordinates.forEach((coordinate) => {
draw(ctx, coordinate)
}
);
});
return [coordinates, setCoordinates, canvasRef, canvasWidth, canvasHeight,counts];
}
Soltion #2 Using Interval
IntervalExample.js (app) 9 sample circle
import React, { useState, useEffect } from 'react';
import Circlo from './Circlo'
const IntervalExample = () => {
const [seconds, setSeconds] = useState(0);
const [circules, setCircules] = useState([]);
let arr =[
{x:19,y:15, r:3,directionX:'left',directionY:'down'},
{x:30,y:10,r:4,directionX:'left',directionY:'down'},
{x:35,y:20,r:5,directionX:'left',directionY:'down'},
{x:0,y:15, r:3,directionX:'left',directionY:'down'},
{x:10,y:30,r:4,directionX:'left',directionY:'down'},
{x:20,y:50,r:5,directionX:'left',directionY:'down'},
{x:70,y:70, r:3,directionX:'left',directionY:'down'},
{x:80,y:80,r:4,directionX:'left',directionY:'down'},
{x:10,y:20,r:5,directionX:'left',directionY:'down'},
]
const reno =(arr)=>{
const table = Array.isArray(arr) && arr.map(item => <Circlo x={item.x} y={item.y} r={item.r} />);
return(table)
}
const speedX = 0.1 // tunne performance by changing this
const speedY = o.1 // tunne performance by changing this
const move = (canvasHeight,canvasWidth) => {
let xarr= arr.map(((coordinate)=> {
let x=coordinate.x;
let y=coordinate.y;
let directionX=coordinate.directionX
let directionY=coordinate.directionY
let r=coordinate.r
if (x < 0) directionX = "right"
if (x > canvasWidth) directionX = "left"
if (y < 0) directionY = "down"
if (y > canvasHeight) directionY = "up"
if (directionX === "left") x -= speedX
else x += speedX
if (directionY === "up") y -= speedY
else y += speedY
return { x:x,y:y,directionX:directionX,directionY:directionY,r:r}
}))
return xarr;
}
useEffect(() => {
const interval = setInterval(() => {
arr =move(100,100)
setCircules( arr)
setSeconds(seconds => seconds + 1);
}, 10);
return () => clearInterval(interval);
}, []);
return (
<div className="App">
<p>
{seconds} seconds have elapsed since mounting.
</p>
<svg viewBox="0 0 100 100">
{ reno(circules)}
</svg>
</div>
);
};
export default IntervalExample;
Circlo.js
import React from 'react';
export default function Circlo(props) {
return (
<circle cx={props.x} cy={props.y} r={props.r} fill="red" />
)
}
First of all, nice effect!
Once said that, I read carefully your code and it seems fine. I'm afraid that the high CPU load is unavoidable with many canvas and transparencies...
To optimize your effect you could try two ways:
try to use only one canvas
try use only CSS, at the end you are using canvas only to draw a filled circle with color from a fixed set: you could use images with pre-drawn same circles and use more or less the same code to simply chage style properties of the images
Probably with a shader you'll be able to obtain the same effect with high CPU save, but unfortunately I'm not proficient on shaders so I can't give you any relevant hint.
Hope I given you some ideas.
Cool effect! I was really surprised that solution proposed by #Sam Erkiner did not perform that much better for me than your original. I would have expected single canvas to be way more efficient.
I decided to try this out with new animation API and pure DOM elements and see how well that works.
Here is my solution(Only changed Circle.js file):
import React, { useEffect, useRef, useMemo } from "react";
import { random } from "lodash";
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;
export default function Circle({ color = null }) {
let ref = useRef();
useEffect(() => {
let y = random(0, HEIGHT);
let x = random(0, WIDTH);
let directionX = random(0, 1) === 0 ? "left" : "right";
let directionY = random(0, 1) === 0 ? "up" : "down";
const speed = 0.5;
const render = () => {
if (x <= 0) directionX = "right";
if (x >= WIDTH) directionX = "left";
if (y <= 0) directionY = "down";
if (y >= HEIGHT) directionY = "up";
let targetX = directionX === 'right' ? WIDTH : 0;
let targetY = directionY === 'down' ? HEIGHT : 0;
const minSideDistance = Math.min(Math.abs(targetX - x), Math.abs(targetY - y));
const duration = minSideDistance / speed;
targetX = directionX === 'left' ? x - minSideDistance : x + minSideDistance;
targetY = directionY === 'up' ? y - minSideDistance : y + minSideDistance;
ref.current.animate([
{ transform: `translate(${x}px, ${y}px)` },
{ transform: `translate(${targetX}px, ${targetY}px)` }
], {
duration: duration,
});
setTimeout(() => {
x = targetX;
y = targetY;
ref.current.style.transform = `translate(${targetX}px, ${targetY}px)`;
}, duration - 10);
setTimeout(() => {
render();
}, duration);
};
render();
}, [color]);
const diameter = useMemo(() => random(0, 0.6 * Math.min(WIDTH, HEIGHT)), []);
return <div style={{
background: color,
position: 'absolute',
width: `${diameter}px`,
height: `${diameter}px`,
top: 0,
left: 0
}} ref={ref} />;
}
Here are performance stats from Safari on my 6 year old Macbook:
Maybe with some additional tweaks could be pushed into green zone?
Your original solution was at the start of red zone, single canvas solution was at the end of yellow zone on Energy impact chart.
I highly recommend reading the article Optimizing the Canvas on the Mozilla Developer's Network website. Specifically, without getting into actual coding, it is not advisable to perform expensive rendering operations repeatedly in the canvas. Alternatively, you can create a virtual canvas inside your circle class and perform the drawing on there when you initially create the circle, then scale your Circle canvas and blit it the main canvas, or blit it and then scale it on the canvas you are blitting to. You can use CanvasRenderingContext2d.getImageData and .putImageData to copy from one canvas to another. How you implement it is up to you, but the idea is not to draw primitives repeatedly when drawing it once and copying the pixel data is pretty fast by comparison.
Update
I tried messing around with your example but I don't have any experience with react so I'm not exactly sure what's going on. Anyway, I cooked up a pure Javascript example without using virtual canvasses, but rather drawing to a canvas, adding it to the document, and animating the canvas itself inside the constraints of the original canvas. This seems to work the fastest and smoothest (Press c to add circles and d to remove circles):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Buffer Canvas</title>
<style>
body, html {
background-color: aquamarine;
padding: 0;
margin: 0;
}
canvas {
border: 1px solid black;
padding: 0;
margin: 0;
box-sizing: border-box;
}
</style>
<script>
function randInt(min, max) {
return min + Math.floor(Math.random() * max);
}
class Circle {
constructor(x, y, r) {
this._canvas = document.createElement('canvas');
this.x = x;
this.y = y;
this.r = r;
this._canvas.width = 2*this.r;
this._canvas.height = 2*this.r;
this._canvas.style.width = this._canvas.width+'px';
this._canvas.style.height = this._canvas.height+'px';
this._canvas.style.border = '0px';
this._ctx = this._canvas.getContext('2d');
this._ctx.beginPath();
this._ctx.ellipse(this.r, this.r, this.r, this.r, 0, 0, Math.PI*2);
this._ctx.fill();
document.querySelector('body').appendChild(this._canvas);
const direction = [-1, 1];
this.vx = 2*direction[randInt(0, 2)];
this.vy = 2*direction[randInt(0, 2)];
this._canvas.style.position = "absolute";
this._canvas.style.left = this.x + 'px';
this._canvas.style.top = this.y + 'px';
this._relativeElem = document.querySelector('body').getBoundingClientRect();
}
relativeTo(elem) {
this._relativeElem = elem;
}
getImageData() {
return this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
}
right() {
return this._relativeElem.left + this.x + this.r;
}
left() {
return this._relativeElem.left + this.x - this.r;
}
top() {
return this._relativeElem.top + this.y - this.r
}
bottom() {
return this._relativeElem.top + this.y + this.r;
}
moveX() {
this.x += this.vx;
this._canvas.style.left = this.x - this.r + 'px';
}
moveY() {
this.y += this.vy;
this._canvas.style.top = this.y - this.r + 'px';
}
move() {
this.moveX();
this.moveY();
}
reverseX() {
this.vx = -this.vx;
}
reverseY() {
this.vy = -this.vy;
}
}
let canvas, ctx, width, height, c, canvasRect;
window.onload = preload;
let circles = [];
function preload() {
canvas = document.createElement('canvas');
canvas.style.backgroundColor = "antiquewhite";
ctx = canvas.getContext('2d');
width = canvas.width = 800;
height = canvas.height = 600;
document.querySelector('body').appendChild(canvas);
canvasRect = canvas.getBoundingClientRect();
document.addEventListener('keypress', function(e) {
if (e.key === 'c') {
let radius = randInt(10, 50);
let c = new Circle(canvasRect.left + canvasRect.width / 2 - radius, canvasRect.top + canvasRect.height / 2 - radius, radius);
c.relativeTo(canvasRect);
circles.push(c);
} else if (e.key === 'd') {
let c = circles.pop();
c._canvas.parentNode.removeChild(c._canvas);
}
});
render();
}
function render() {
// Draw
ctx.clearRect(0, 0, canvas.width, canvas.height);
circles.forEach((c) => {
// Check position and change direction if we hit the edge
if (c.left() <= canvasRect.left || c.right() >= canvasRect.right) {
c.reverseX();
}
if (c.top() <= canvasRect.top || c.bottom() >= canvasRect.bottom) {
c.reverseY();
}
// Update position for next render
c.move();
});
requestAnimationFrame(render);
}
</script>
</head>
<body>
</body>
</html>

three.js camera rotating by touch events and deviceorientation event

I'm trying to build a webapp that let users watch 360 panorama images with three.js, but some of the code isn't working as expected. The problem involves rotating the camera.
I'm able to rotate the camera by listening to either touch events or deviceorientation events but I can't manage to make both work simultaneously.
Currently, the camera rotates by touch gesture, but it snaps back immediately after detecting a device orientation event.
I want the camera to rotate with touch gestures and then rotate with device orientation starting from the rotation it was previously set by touch.
I think I should improve deviceorientation event handler or setQuaternion method below in add-gestures.js but I don't know how.
index.html
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="pages/vr/style.css') ?>
</head>
<body>
<canvas id="vr-canvas"></canvas>
<script src="/js/lib/threejs/r104/build/three.min.js"></script>
<script src="/js/pages/vr/init-vr.js"></script>
<script src="/js/pages/vr/add-gestures.js"></script>
<script src="/js/pages/vr/add-sphere.js"></script>
</body>
</html>
init-vr.js
window.VRApp = window.VRApp || {};
const canvas = document.querySelector("#vr-canvas");
const renderer = (() => {
const webGLRenderer = new THREE.WebGLRenderer({ canvas });
webGLRenderer.setPixelRatio(window.devicePixelRatio);
return webGLRenderer;
})();
const scene = new THREE.Scene();
const camera = (() => {
const perspectiveCamera = new THREE.PerspectiveCamera(100, canvas.width / canvas.height, 0.01, 100);
perspectiveCamera.rotation.order = "ZYX";
return perspectiveCamera;
})();
const animate = () => {
requestAnimationFrame(animate);
renderer.render(scene, camera);
};
animate();
window.VRApp.renderer = renderer;
window.VRApp.scene = scene;
window.VRApp.camera = camera;
add-gestures.js
window.VRApp = window.VRApp || {};
const State = {
Neutral: 0x0000,
RotateCamera: 0x0001,
};
let state = State.Neutral;
let windowOrientation = window.orientation || 0;
let cameraRotationCache = window.VRApp.camera.rotation.clone();
let mousePositionCache = {
x: 0,
y: 0,
minYDiff: 0,
maxYDiff: 0,
};
const setState = (newState) => {
if (State.hasOwnProperty(newState)) {
state = State[newState];
}
};
const checkState = (targetState) => {
if (State.hasOwnProperty(targetState)) {
return state === State[targetState];
}
return false;
};
const setQuaternion = (() => {
const zee = new THREE.Vector3(0, 0, 1);
const euler = new THREE.Euler();
const q0 = new THREE.Quaternion();
const q1 = new THREE.Quaternion(-1 * Math.sqrt(0.5), 0, 0, Math.sqrt(0.5));
return (alpha, beta, gamma, orientation) => {
euler.set(beta, alpha, -1 * gamma, "YXZ");
window.VRApp.camera.quaternion.setFromEuler(euler);
window.VRApp.camera.quaternion.multiply(q1);
window.VRApp.camera.quaternion.multiply(q0.setFromAxisAngle(zee, -1 * orientation));
};
})();
const onMouseDownHandler = (clientX, clientY) => {
setState("RotateCamera");
cameraRotationCache = window.VRApp.camera.rotation.clone();
mousePositionCache.x = clientX;
mousePositionCache.y = clientY;
mousePositionCache.minYDiff = -90 - (cameraRotationCache.x * (180 / Math.PI)) - (clientY * (Math.PI / 180));
mousePositionCache.maxYDiff = 90 - (cameraRotationCache.x * (180 / Math.PI)) - (clientY * (Math.PI / 180));
};
const onMouseMoveHandler = (clientX, clientY) => {
if (checkState("RotateCamera")) {
window.VRApp.camera.rotation.order = "ZYX";
let xDiff = clientX - mousePositionCache.x;
let yDiff = clientY - mousePositionCache.y;
if (yDiff < mousePositionCache.minYDiff) {
yDiff = mousePositionCache.minYDiff;
mousePositionCache.y = clientY - mousePositionCache.minYDiff;
}
if (yDiff > mousePositionCache.maxYDiff) {
yDiff = mousePositionCache.maxYDiff;
mousePositionCache.y = clientY - mousePositionCache.maxYDiff;
}
let newAngleX = cameraRotationCache.x + (yDiff * (Math.PI / 180));
let newAngleY = cameraRotationCache.y + (xDiff * (Math.PI / 180));
window.VRApp.camera.rotation.x = newAngleX;
window.VRApp.camera.rotation.y = newAngleY;
}
};
const onMouseUpHandler = () => {
setState("Neutral");
cameraRotationCache = window.VRApp.camera.rotation.clone();
mousePositionCache.x = 0;
mousePositionCache.y = 0;
mousePositionCache.minYDiff = 0;
mousePositionCache.maxYDiff = 0;
};
if ("onresize" in window) {
window.addEventListener("resize", (event) => {
const width = window.innerWidth;
const height = window.innerHeight;
window.VRApp.renderer.domElement.width = width;
window.VRApp.renderer.domElement.height = height;
window.VRApp.renderer.domElement.style.height = height + "px";
window.VRApp.renderer.setSize(width, height);
window.VRApp.camera.aspect = width / height;
window.VRApp.camera.updateProjectionMatrix();
});
}
if ("onload" in window) {
window.addEventListener("load", (event) => {
const width = window.innerWidth;
const height = window.innerHeight;
window.VRApp.renderer.domElement.width = width;
window.VRApp.renderer.domElement.height = height;
window.VRApp.renderer.domElement.style.height = height + "px";
window.VRApp.renderer.setSize(width, height);
window.VRApp.camera.aspect = width / height;
window.VRApp.camera.updateProjectionMatrix();
});
}
if ("onmousedown" in window.VRApp.renderer.domElement) {
window.VRApp.renderer.domElement.addEventListener("mousedown", (event) => {
onMouseDownHandler(event.clientX, event.clientY);
});
}
if ("onmousemove" in window.VRApp.renderer.domElement) {
window.VRApp.renderer.domElement.addEventListener("mousemove", (event) => {
onMouseMoveHandler(event.clientX, event.clientY);
});
}
if ("onmouseup" in window.VRApp.renderer.domElement) {
window.VRApp.renderer.domElement.addEventListener("mouseup", (event) => {
onMouseUpHandler();
});
}
if ("onmouseleave" in window.VRApp.renderer.domElement) {
window.VRApp.renderer.domElement.addEventListener("mouseleave", (event) => {
onMouseUpHandler();
});
}
if ("ontouchstart" in window.VRApp.renderer.domElement) {
window.VRApp.renderer.domElement.addEventListener("touchstart", (event) => {
event.preventDefault();
if (event.touches.length === 1) {
const touch = event.touches[0];
onMouseDownHandler(touch.clientX, touch.clientY);
}
});
}
if ("ontouchmove" in window.VRApp.renderer.domElement) {
window.VRApp.renderer.domElement.addEventListener("touchmove", (event) => {
event.preventDefault();
if (event.touches.length === 1) {
const touch = event.touches[0];
onMouseMoveHandler(touch.clientX, touch.clientY);
}
});
}
if ("ontouchend" in window.VRApp.renderer.domElement) {
window.VRApp.renderer.domElement.addEventListener("touchend", (event) => {
event.preventDefault();
onMouseUpHandler();
});
}
if ("ontouchcancel" in window.VRApp.renderer.domElement) {
window.VRApp.renderer.domElement.addEventListener("touchcancel", (event) => {
event.preventDefault();
onMouseUpHandler();
});
}
if ("onorientationchange" in window) {
window.addEventListener("orientationchange", (event) => {
windowOrientation = window.orientation || 0;
});
}
if ("ondeviceorientation" in window) {
window.addEventListener("deviceorientation", (event) => {
if (checkState("Neutral")) {
let alpha = event.alpha * (Math.PI / 180);
let beta = event.beta * (Math.PI / 180);
let gamma = event.gamma * (Math.PI / 180);
let orientation = windowOrientation * (Math.PI / 180);
setQuaternion(alpha, beta, gamma, orientation);
}
});
}
add-sphere.js
window.VRApp = window.VRApp || {};
const sphere = (() => {
const geometry = new THREE.SphereGeometry(100, 64, 64);
geometry.scale(1, 1, -1);
geometry.rotateY(Math.PI / 2);
const material = new THREE.MeshBasicMaterial({
});
const mesh = new THREE.Mesh(geometry, material);
return mesh;
})();
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load("/img/pages/vr/sample-360.jpg");
sphere.material.map = texture;
window.VRApp.scene.add(sphere);

Categories

Resources