I have got the following code:
const linksGraphics = new PIXI.Graphics();
const update = () => {
linksGraphics.clear();
linksGraphics.alpha = 1;
if (forceLinkActive) {
data.links.forEach(link => {
let { source, target } = link;
linksGraphics.lineStyle(2, 0x000000);
linksGraphics.moveTo(source.x, source.y);
linksGraphics.lineTo(target.x, target.y);
});
linksGraphics.endFill();
} }
app.ticker.add( () => update() );
Where data.links is an array of edge data {source: number, target: number}. If I understand right, all lines are part of the PIXI.Graphics object. But what I need:
every line should have own opacity
every line should have an event for mouse over
Any ideas how modify my code?
Thanks.
It's been a while but can make a suggestion. Lines do not react to mouse/pointer over events in pixijs.
Instead you may want to accompany a transformed rectangle with alpha value 0 and listen mouse/pointer with this rectangle.
For example lets, change the alpha value of the line when mouse/pointer hovers the accompanying rectangle.
const app = new PIXI.Application({
width: window.innerWidth,
height: window.innerHeight,
backgroundColor: 0x283230
});
document.body.appendChild(app.view);
// 1. PRELIMINARY COMPUTATIONS
// Coordinates of the end points of a line
let x0 = 100;
let y0 = 100;
let x1 = 200;
let y1 = 200;
// Find midpoint for translation
let xmid = 0.5*(x0+x1);
let ymid = 0.5*(y0+y1);
// Length of the line
let length = Math.hypot(x0-x1, y0-y1);
// Alignment angle of the line, i.e. angle with the x axis
let angle = Math.atan((y1-y0)/(x1-x0));
// 2. LINE
line = new PIXI.Graphics();
// Arbitrary line style, say we have a non-white background
line.lineStyle(8,0xffffff,1);
line.moveTo(x0,y0);
line.lineTo(x1,y1);
// 3. ACCOMPANYING RECTANGLE
line.rectangle = new PIXI.Graphics();
line.rectangle.beginFill(0xffffff);
// Since we are going to translate, think of 0,0 is the center point on the rectangle
// Width of the rectangle is selected arbitrarily as 30
const width = 30;
line.rectangle.drawRect(-length/2,-width/2,length,width);
line.rectangle.endFill();
line.rectangle.alpha = 0;
line.rectangle.interactive = true;
line.rectangle.on("pointerover", reactOver);
line.rectangle.on("pointerout", reactOut);
// Apply transformation
line.rectangle.setTransform(xmid, ymid,1,1,angle);
app.stage.addChild(line);
// Add rectangle to the stage too.
app.stage.addChild(line.rectangle);
// Let's change alpha value of the line when user hovers.
function reactOver(){
line.alpha = 0.5;
}
function reactOut(){
line.alpha = 1;
}
To the PEN, Hover a line in pixijs
We can expand this logic to a rectangle for instance. But this time you need two accompanying rectangles (with alpha=0) where one of them is wider and the other is narrower than the unfilled rectangle. For example,
const app = new PIXI.Application({
width: window.innerWidth,
height: window.innerHeight,
backgroundColor: 0x283230
});
document.body.appendChild(app.view);
const x = 100;
const y = 100;
const width = 150;
const height = 100;
const hoverWidth = 20;
const rect = new PIXI.Graphics();
rect.lineStyle(4, 0xffffff,1);
rect.drawRect(x,y,width,height);
rect.outer = new PIXI.Graphics();
rect.inner = new PIXI.Graphics();
// Fill outer
rect.outer.alpha = 0;
rect.outer.beginFill(0xffffff);
rect.outer.drawRect(x-hoverWidth/2, y-hoverWidth/2, width+hoverWidth, height+hoverWidth);
rect.outer.endFill();
// Fill inner
rect.inner.alpha = 0;
rect.inner.beginFill(0xffffff);
rect.inner.drawRect(x+hoverWidth/2, y+hoverWidth/2, width-hoverWidth, height-hoverWidth);
rect.inner.endFill();
// Add interaction and listeners
rect.outer.interactive = true;
rect.inner.interactive = true;
rect.outer.on("pointerover", pOverOuter);
rect.outer.on("pointerout", pOutOuter);
rect.inner.interaction = true;
rect.inner.on("pointerover", pOverInner);
rect.inner.on("pointerout", pOutInner);
app.stage.addChild(rect);
app.stage.addChild(rect.outer);
app.stage.addChild(rect.inner);
// Listeners
let overOuter = false;
let overInner = false;
function pOverOuter(){
overOuter = true;
changeAlpha();
// rect.alpha = 0.5;
}
function pOutOuter(){
overOuter = false;
changeAlpha();
}
function pOverInner(){
overInner = true;
changeAlpha();
// rect.alpha = 1;
}
function pOutInner(){
overInner = false;
changeAlpha();
// rect.alpha = 0.5;
}
function changeAlpha(){
rect.alpha = (overOuter && !overInner)? 0.5: 1;
}
To the PEN, Hover a rectangle in pixijs
For your first requirement, try creating separate graphics objects for drawing each line and set alpha for each line.
For your second requirement, You need to set the interactive property of graphics (linksGraphics) object to true like below,
linksGraphics.interactive = true;
and then attach a function to be executed on mouseover event like below,
var mouseOverAction = function () {
//Some code
};
linksGraphics.on('mouseover', mouseOverAction);
You can define a hitArea on a graphic. And with getBounds() you can make a line clickable. After you do that you can also assign pointerEvents to the graphic.
const linksGraphics = new PIXI.Graphics();
const update = () => {
linksGraphics.clear();
linksGraphics.alpha = 1;
if (forceLinkActive) {
data.links.forEach(link => {
let { source, target } = link;
linksGraphics.lineStyle(2, 0x000000);
linksGraphics.moveTo(source.x, source.y);
linksGraphics.lineTo(target.x, target.y);
//A line itself is not clickable
linksGraphics.hitArea = linksGraphics.getBounds();
});
linksGraphics.endFill();
}
}
app.ticker.add( () => update() );
Related
I've got the below code, which takes in a number of scans. I know this to always be 2880. The canvas should split an entire 360° into 2880 sectors. The loop in the code below will always run from 0 to 2880, and in each loop, a bunch of (maybe several hundred) 2px coloured points are rendered in that sector, emanating from the centre of the canvas outward. The loop moves fast, before I upgraded the THREE package, this loop could render in c. 15 seconds.
The picture draws correctly, but what confuses me is the fact that the call to THREE's render message happens inside of the loop, yet the picture draws nothing until the last iteration of the loop is complete and then all 2880 sectors appear at once, which isn't the effect I'm going for.
Can anyone advise what I might be missing? It's a 2-D non-interactable image.
Stuff I've tried:-
setTimeout(null, 1000) after the .render() method to make it wait before executing the next iteration of the loop
Considered making it a recursive function with the next iteration of the loop inside of the above setTimeout
Reversing the THREE upgrade as an absolute last resort.
Stuff I've considered:-
Is the loop running two fast for the frame rate or not giving the screen enough time to update?
Limitation of THREEJs?
const drawReflectivityMap = (scans, reflectivityData, azimuthData, scene, renderer, totalScans, currentScan, cameraPosition, camera, reflectivityColours) => {
currentCamera = camera;
currentRenderer = renderer;
for (let i = 0; i < scans; i++) {
console.log('Drawing Reflectivity ' + i);
var reflectivity = reflectivityData[i];
var azimuth = utils.radians(azimuthData[i]);
var sinMultiplier = Math.sin(azimuth);
var cosMultiplier = Math.cos(azimuth);
var initialRange = mikeUtilities.addRange(mikeUtilities.multiplyRange(mikeUtilities.createRange(0, reflectivity.GateCount, 1), reflectivity.GateSize), reflectivity.FirstGate);
var x = utils.multiplyRange(initialRange, sinMultiplier);
var y = utils.multiplyRange(initialRange, cosMultiplier);
var dataSet = {
x: x,
y: y,
reflectivity: reflectivity
};
var reflectivityColourScale = d3.scaleQuantize().domain([-32.0, 94.5]).range(reflectivityColours);
var pointsMaterial = new THREE.PointsMaterial({
size: 2,
vertexColors: true,
sizeAttenuation: false
});
// x co-ordinate points for each point in this arc
var x = dataSet.x;
// y co-ordinate points for each point in this arc
var y = dataSet.y;
// Reflectivity (rainfall) intensity values for each point in this arc
var reflectivity = dataSet.reflectivity;
var geometry = new THREE.BufferGeometry();
var pointsGraph = [];
var coloursGraph = [];
x.forEach(function (index, i) {
if (reflectivity.MomentDataValues[i] > -33) {
geometry = new THREE.BufferGeometry();
var dataPointColour = new THREE.Color(reflectivityColourScale(reflectivity.MomentDataValues[i]));
pointsGraph.push(x[i], y[i], 0);
coloursGraph.push(dataPointColour.r, dataPointColour.g, dataPointColour.b);
}
});
var pointsGraphArray = new Float32Array(pointsGraph);
var coloursGraphArray = new Float32Array(coloursGraph);
geometry.setAttribute('position', new THREE.BufferAttribute(pointsGraphArray, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(coloursGraphArray, 3));
var pointsMap = new THREE.Points(geometry, pointsMaterial);
scene.add(pointsMap);
renderScene(scene, cameraPosition, renderer);
}
}
function renderScene(scene, cameraPosition,renderer) {
currentCamera.position.z = cameraPosition;
currentRenderer.render(scene, currentCamera);
requestAnimationFrame(renderScene);
}
for (let i = 0; i < scans; i++) {
setTimeout(() => {
// Your code goes here
} i * 100)
}
sorry for the amatuer question, I'm really new to Pixi.js
Well, I'm trying to make a scratch card.
There will be two image, one for background("A"), and one for hidden background( "B" , I think this is mask, but I'm not sure about this terminology , so to avoid confusion I'll describe as hidden background)
So, image 'A' will cover up image 'B'.
Then, I'will use bristle image for the brush.
When I execute dragging event on Image "A" , I want to show up image 'B" along path mouse moved.
Sorry , for my terrible description. Think I'm cloning this website ( https://art4globalgoals.com/en )
I followed this example( https://pixijs.io/examples/#/demos-advanced/scratchcard.js ), and succeed to make a brush effect, with only 1 image. But couldn't implement it with two image.
Precisely speaking , when I run my app, there is black screen rendered , and if I scratch screen, the hidden Image appears.
but the problem is that, I want to set other Image instead of black screen.
below image is my current status , which will happen when running below code
import * as PIXI from 'pixi.js';
// const canvas = document.getElementById('webgl');
const screenSize = {
width: window.innerWidth,
height: window.innerHeight
};
let brushWidth = (window.innerHeight / window.innerWidth) * 200;
let brushHeight = (window.innerHeight / window.innerWidth) * 200;
// canvas.width = screenSize.width;
// canvas.height = screenSize.height;
const app = new PIXI.Application({
width: window.innerWidth,
height: window.innerHeight,
resolution: window.devicePixelRatio,
autoDensity: true
});
document.body.appendChild(app.view);
app.loader
.add('background', '/png/effel.png')
.add('mask', '/png/effel-gray.png')
.add('bristle1', '/png/brush6.png')
.add('bristle2', '/png/bristle2.png')
.load(() => {
setup();
});
const setup = () => {
const brushTexture = app.loader.resources.bristle1.texture;
const brushTexture2 = app.loader.resources.bristle2.texture;
const brush = new PIXI.Sprite(brushTexture);
const brush2 = new PIXI.Sprite(brushTexture2);
brush.width = brushWidth;
brush.height = brushHeight;
brush2.width = brushWidth;
brush2.height = brushHeight;
const backgroundTexture = app.loader.resources.background.texture;
const maskTexture = app.loader.resources.mask.texture;
const background = new PIXI.Sprite(backgroundTexture);
background.x = app.renderer.screen.width / 2;
background.y = app.renderer.screen.height / 2;
background.anchor.x = 0.5;
background.anchor.y = 0.5;
background.width = window.innerWidth;
background.height = window.innerHeight;
const Mask = new PIXI.Sprite(maskTexture);
Mask.width = app.renderer.screen.width;
Mask.height = app.renderer.screen.height;
Mask.x = app.renderer.screen.width / 2;
Mask.y = app.renderer.screen.height / 2;
Mask.anchor.x = 0.5;
Mask.anchor.y = 0.5;
Mask.width = window.innerWidth;
Mask.height = window.innerHeight;
// app.stage.addChild(background);
app.stage.addChild(Mask);
const renderTexture = PIXI.RenderTexture.create(app.screen.width, app.screen.height);
const renderTextureSprite = new PIXI.Sprite(renderTexture);
app.stage.addChild(renderTextureSprite);
Mask.mask = renderTextureSprite;
app.stage.interactive = true;
app.stage.on('pointerdown', pointerDown);
app.stage.on('pointerup', pointerUp);
app.stage.on('pointermove', pointerMove);
let dragging = false;
let bristle2Render = false;
function pointerMove(event) {
if (dragging) {
brush.position.copyFrom(event.data.global);
// brushWidth += 0.5;
// brush.width = brushWidth;
if (!bristle2Render) {
setTimeout(() => (bristle2Render = true), 500);
}
app.renderer.render(brush, renderTexture, false, null, false);
if (bristle2Render) {
brush2.position.copyFrom(event.data.global);
app.renderer.render(brush2, renderTexture, false, null, false);
}
// if (brush.width === 100) {
// dragging = false;
// brushWidth = 0;
// }
}
}
function pointerDown(event) {
dragging = true;
console.log(11);
pointerMove(event);
}
function pointerUp(event) {
dragging = false;
bristle2Render = false;
}
window.addEventListener('resize', () => {
screenSize.width = window.innerWidth;
screenSize.height = window.innerHeight;
app.renderer.resize(window.innerWidth, window.innerHeight);
app.renderer.stage.width = window.innerWidth;
app.renderer.stage.height = window.innerHeight;
});
};
// const backgroundTexture = PIXI.Texture.from('/png/effel.png');
// const maskTexture = PIXI.Texture.from('/png/effel-gray.png');
I guess the problem is that you haven't added background to the stage, because you commented it.
// app.stage.addChild(background);
app.stage.addChild(Mask);
So, there is nothing to be shown at the beginning, so it is a black screen.
Here is a CodePen that I created. I removed some comments and replaced the resources with some other images. It should be somehow working after you uncommented the line.
By the way, you may want to ensure that:
your variables have a capital name only for some special reasons (e.g. Mask)
you don't overwrite values that you have set before (e.g. Mask.width = ...).
Although the code still works without fixing the above, this may make the code harder to work with for others, including a future "you". ;)
When your code works, you may want to post it on Code Review to see how to improve the code.
Goal
To get this mini-game working in next.js powered environment 'the right way'.
Background
The linked game uses Three.js which in turn requires the window object to update certain variables according to the game logic.
And next.js does not have a window object defined on the server side, as explained here.
So, to tackle this problem, I moved the window object inside of useEffect hook provided by react as mentioned in this article.
Problem
Moving window inside of useEffect worked but other helper variables depends on its value, so to make it all work without errors, I had to move the entire javascript code with some minor adjustments inside of useEffect of a component page that I created.
Extra
I started this project by trying to recreate this game using react-three-fiber as it abstracts well with Next.js but I had to change my ways because the documentation was not clear and lacks example code.
To even change the default camera setting from perspective to orthographic inside of Canvas tag was a little unruly.
But as explained by Bruno Simon, Paul Henschel and others, three becomes quite hard to manage/scale/change/reuse when the code becomes complex.
And I wish to maintain the component property of my code.
How shall I go about it?
Thankyou for any leads, references to courses, articles.
This stackoverflow thread poses a similar question to mine but I have my what ifs.
Code
import styles from '../styles/Home.module.css'
import { React, useState, useLayoutEffect, useRef, useEffect } from 'react'
import * as THREE from 'three'
import * as CANNON from 'cannon'
export default function Home() {
useEffect(() => {
window.focus(); // Capture keys right away (by default focus is on editor)
let camera, scene, renderer; // ThreeJS globals
let world; // CannonJs world
let lastTime; // Last timestamp of animation
let stack; // Parts that stay solid on top of each other
let overhangs; // Overhanging parts that fall down
const boxHeight = 1; // Height of each layer
const originalBoxSize = 3; // Original width and height of a box
let autopilot;
let gameEnded;
let robotPrecision; // Determines how precise the game is on autopilot
const scoreElement = document.getElementById("score");
const instructionsElement = document.getElementById("instructions");
const resultsElement = document.getElementById("results");
init();
// Determines how precise the game is on autopilot
function setRobotPrecision() {
robotPrecision = Math.random() * 1 - 0.5;
}
function init() {
autopilot = true;
gameEnded = false;
lastTime = 0;
stack = [];
overhangs = [];
setRobotPrecision();
// Initialize CannonJS
world = new CANNON.World();
world.gravity.set(0, -10, 0); // Gravity pulls things down
world.broadphase = new CANNON.NaiveBroadphase();
world.solver.iterations = 40;
// Initialize ThreeJs
const aspect = window.innerWidth / window.innerHeight;
const width = 10;
const height = width / aspect;
camera = new THREE.OrthographicCamera(
width / -2, // left
width / 2, // right
height / 2, // top
height / -2, // bottom
0, // near plane
100 // far plane
);
/*
// If you want to use perspective camera instead, uncomment these lines
camera = new THREE.PerspectiveCamera(
45, // field of view
aspect, // aspect ratio
1, // near plane
100 // far plane
);
*/
camera.position.set(4, 4, 4);
camera.lookAt(0, 0, 0);
scene = new THREE.Scene();
// Foundation
addLayer(0, 0, originalBoxSize, originalBoxSize);
// First layer
addLayer(-10, 0, originalBoxSize, originalBoxSize, "x");
// Set up lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
dirLight.position.set(10, 20, 0);
scene.add(dirLight);
// Set up renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animation);
document.body.appendChild(renderer.domElement);
}
function startGame() {
autopilot = false;
gameEnded = false;
lastTime = 0;
stack = [];
overhangs = [];
if (instructionsElement) instructionsElement.style.display = "none";
if (resultsElement) resultsElement.style.display = "none";
if (scoreElement) scoreElement.innerText = 0;
if (world) {
// Remove every object from world
while (world.bodies.length > 0) {
world.remove(world.bodies[0]);
}
}
if (scene) {
// Remove every Mesh from the scene
while (scene.children.find((c) => c.type == "Mesh")) {
const mesh = scene.children.find((c) => c.type == "Mesh");
scene.remove(mesh);
}
// Foundation
addLayer(0, 0, originalBoxSize, originalBoxSize);
// First layer
addLayer(-10, 0, originalBoxSize, originalBoxSize, "x");
}
if (camera) {
// Reset camera positions
camera.position.set(4, 4, 4);
camera.lookAt(0, 0, 0);
}
}
function addLayer(x, z, width, depth, direction) {
const y = boxHeight * stack.length; // Add the new box one layer higher
const layer = generateBox(x, y, z, width, depth, false);
layer.direction = direction;
stack.push(layer);
}
function addOverhang(x, z, width, depth) {
const y = boxHeight * (stack.length - 1); // Add the new box one the same layer
const overhang = generateBox(x, y, z, width, depth, true);
overhangs.push(overhang);
}
function generateBox(x, y, z, width, depth, falls) {
// ThreeJS
const geometry = new THREE.BoxGeometry(width, boxHeight, depth);
const color = new THREE.Color(`hsl(${30 + stack.length * 4}, 100%, 50%)`);
const material = new THREE.MeshLambertMaterial({ color });
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, y, z);
scene.add(mesh);
// CannonJS
const shape = new CANNON.Box(
new CANNON.Vec3(width / 2, boxHeight / 2, depth / 2)
);
let mass = falls ? 5 : 0; // If it shouldn't fall then setting the mass to zero will keep it stationary
mass *= width / originalBoxSize; // Reduce mass proportionately by size
mass *= depth / originalBoxSize; // Reduce mass proportionately by size
const body = new CANNON.Body({ mass, shape });
body.position.set(x, y, z);
world.addBody(body);
return {
threejs: mesh,
cannonjs: body,
width,
depth
};
}
function cutBox(topLayer, overlap, size, delta) {
const direction = topLayer.direction;
const newWidth = direction == "x" ? overlap : topLayer.width;
const newDepth = direction == "z" ? overlap : topLayer.depth;
// Update metadata
topLayer.width = newWidth;
topLayer.depth = newDepth;
// Update ThreeJS model
topLayer.threejs.scale[direction] = overlap / size;
topLayer.threejs.position[direction] -= delta / 2;
// Update CannonJS model
topLayer.cannonjs.position[direction] -= delta / 2;
// Replace shape to a smaller one (in CannonJS you can't simply just scale a shape)
const shape = new CANNON.Box(
new CANNON.Vec3(newWidth / 2, boxHeight / 2, newDepth / 2)
);
topLayer.cannonjs.shapes = [];
topLayer.cannonjs.addShape(shape);
}
window.addEventListener("mousedown", eventHandler);
window.addEventListener("touchstart", eventHandler);
window.addEventListener("keydown", function (event) {
if (event.key == " ") {
event.preventDefault();
eventHandler();
return;
}
if (event.key == "R" || event.key == "r") {
event.preventDefault();
startGame();
return;
}
});
function eventHandler() {
if (autopilot) startGame();
else splitBlockAndAddNextOneIfOverlaps();
}
function splitBlockAndAddNextOneIfOverlaps() {
if (gameEnded) return;
const topLayer = stack[stack.length - 1];
const previousLayer = stack[stack.length - 2];
const direction = topLayer.direction;
const size = direction == "x" ? topLayer.width : topLayer.depth;
const delta =
topLayer.threejs.position[direction] -
previousLayer.threejs.position[direction];
const overhangSize = Math.abs(delta);
const overlap = size - overhangSize;
if (overlap > 0) {
cutBox(topLayer, overlap, size, delta);
// Overhang
const overhangShift = (overlap / 2 + overhangSize / 2) * Math.sign(delta);
const overhangX =
direction == "x"
? topLayer.threejs.position.x + overhangShift
: topLayer.threejs.position.x;
const overhangZ =
direction == "z"
? topLayer.threejs.position.z + overhangShift
: topLayer.threejs.position.z;
const overhangWidth = direction == "x" ? overhangSize : topLayer.width;
const overhangDepth = direction == "z" ? overhangSize : topLayer.depth;
addOverhang(overhangX, overhangZ, overhangWidth, overhangDepth);
// Next layer
const nextX = direction == "x" ? topLayer.threejs.position.x : -10;
const nextZ = direction == "z" ? topLayer.threejs.position.z : -10;
const newWidth = topLayer.width; // New layer has the same size as the cut top layer
const newDepth = topLayer.depth; // New layer has the same size as the cut top layer
const nextDirection = direction == "x" ? "z" : "x";
if (scoreElement) scoreElement.innerText = stack.length - 1;
addLayer(nextX, nextZ, newWidth, newDepth, nextDirection);
} else {
missedTheSpot();
}
}
function missedTheSpot() {
const topLayer = stack[stack.length - 1];
// Turn to top layer into an overhang and let it fall down
addOverhang(
topLayer.threejs.position.x,
topLayer.threejs.position.z,
topLayer.width,
topLayer.depth
);
world.remove(topLayer.cannonjs);
scene.remove(topLayer.threejs);
gameEnded = true;
if (resultsElement && !autopilot) resultsElement.style.display = "flex";
}
function animation(time) {
if (lastTime) {
const timePassed = time - lastTime;
const speed = 0.008;
const topLayer = stack[stack.length - 1];
const previousLayer = stack[stack.length - 2];
// The top level box should move if the game has not ended AND
// it's either NOT in autopilot or it is in autopilot and the box did not yet reach the robot position
const boxShouldMove =
!gameEnded &&
(!autopilot ||
(autopilot &&
topLayer.threejs.position[topLayer.direction] <
previousLayer.threejs.position[topLayer.direction] +
robotPrecision));
if (boxShouldMove) {
// Keep the position visible on UI and the position in the model in sync
topLayer.threejs.position[topLayer.direction] += speed * timePassed;
topLayer.cannonjs.position[topLayer.direction] += speed * timePassed;
// If the box went beyond the stack then show up the fail screen
if (topLayer.threejs.position[topLayer.direction] > 10) {
missedTheSpot();
}
} else {
// If it shouldn't move then is it because the autopilot reached the correct position?
// Because if so then next level is coming
if (autopilot) {
splitBlockAndAddNextOneIfOverlaps();
setRobotPrecision();
}
}
// 4 is the initial camera height
if (camera.position.y < boxHeight * (stack.length - 2) + 4) {
camera.position.y += speed * timePassed;
}
updatePhysics(timePassed);
renderer.render(scene, camera);
}
lastTime = time;
}
function updatePhysics(timePassed) {
world.step(timePassed / 1000); // Step the physics world
// Copy coordinates from Cannon.js to Three.js
overhangs.forEach((element) => {
element.threejs.position.copy(element.cannonjs.position);
element.threejs.quaternion.copy(element.cannonjs.quaternion);
});
}
window.addEventListener("resize", () => {
// Adjust camera
console.log("resize", window.innerWidth, window.innerHeight);
const aspect = window.innerWidth / window.innerHeight;
const width = 10;
const height = width / aspect;
camera.top = height / 2;
camera.bottom = height / -2;
// Reset renderer
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.render(scene, camera);
});
})
return (
<div className={styles.container}>
<div className={styles.nav}>
<h2>Ritik Jangir</h2>
</div>
</div>
)
}
I'm using Pixi with PixiOverlay on leaflet. I have the following jsfiddle for a dummy simulation. The objective: once you click Add Image 2 - it adds a picture of a hamster randomly on the map.
It (almost) work.
the problem:
Error message: "BaseTexture added to the cache with an id [hamster] that already had an entry"
I couldn't figure our where to put the loader and how to integrate it properly in terms of code organization: (do I need to use it only once?) what if I have other layers to add? So I assume my challenge is here:
this.loader.load((loader, resources) => {...}
Minor: how to reduce the size of the hamster :-)
my JS code (also on jsfiddle)
class Simulation
{
constructor()
{
// center of the map
var center = [1.8650, 51.2094];
// Create the map
this.map = L.map('map').setView(center, 2);
// Set up the OSM layer
L.tileLayer(
'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18
}).addTo(this.map);
this.imagesLayer = new L.layerGroup();
this.imagesLayer.addTo(this.map);
}
_getRandomCoord()
{
var randLat = Math.floor(Math.random() * 90);
randLat *= Math.round(Math.random()) ? 1 : -1;
var randLon = Math.floor(Math.random() * 180);
randLon *= Math.round(Math.random()) ? 1 : -1;
return [randLat,randLon]
}
addImage2()
{
this.loader = new PIXI.Loader()
this.loader.add('hamster', 'https://cdn-icons-png.flaticon.com/512/196/196817.png')
this.loader.load((loader, resources) => {
let markerTexture = resources.hamster.texture
let markerLatLng = this._getRandomCoord()
let marker = new PIXI.Sprite(markerTexture)
marker.anchor.set(0.5, 1)
let pixiContainer = new PIXI.Container()
pixiContainer.addChild(marker)
let firstDraw = true
let prevZoom
let pixiOverlay = L.pixiOverlay(utils => {
let zoom = utils.getMap().getZoom()
let container = utils.getContainer()
let renderer = utils.getRenderer()
let project = utils.latLngToLayerPoint
let scale = utils.getScale()
if (firstDraw) {
let markerCoords = project(markerLatLng)
marker.x = markerCoords.x
marker.y = markerCoords.y
}
if (firstDraw || prevZoom !== zoom) {
marker.scale.set(1 / scale)
}
firstDraw = true
prevZoom = zoom
renderer.render(container)
}, pixiContainer)
this.imagesLayer.addLayer(pixiOverlay);
})
}
addTriangle()
{
console.log("Trinalge")
var polygonLatLngs = [
[51.509, -0.08],
[51.503, -0.06],
[51.51, -15.047],
[21.509, -0.08]
];
var projectedPolygon;
var triangle = new PIXI.Graphics();
var pixiContainer = new PIXI.Container();
pixiContainer.addChild(triangle);
var firstDraw = true;
var prevZoom;
var pixiOverlay = L.pixiOverlay(function(utils) {
var zoom = utils.getMap().getZoom();
var container = utils.getContainer();
var renderer = utils.getRenderer();
var project = utils.latLngToLayerPoint;
var scale = utils.getScale();
if (firstDraw) {
projectedPolygon = polygonLatLngs.map(function(coords) {return project(coords);});
}
if (firstDraw || prevZoom !== zoom) {
triangle.clear();
triangle.lineStyle(3 / scale, 0x3388ff, 1);
triangle.beginFill(0x3388ff, 0.2);
projectedPolygon.forEach(function(coords, index) {
if (index == 0) triangle.moveTo(coords.x, coords.y);
else triangle.lineTo(coords.x, coords.y);
});
triangle.endFill();
}
firstDraw = false;
prevZoom = zoom;
renderer.render(container);
}.bind(this), pixiContainer);
this.imagesLayer.addLayer(pixiOverlay)
}
removeLayer()
{
this.imagesLayer.clearLayers();
}
}
var simulation = new Simulation();
TLDR: Updated jsfiddle:
https://jsfiddle.net/gbsdfm97/
more info below:
First problem: loading resources (textures)
There was error in console because you loaded hamster image on each click:
addImage2()
{
this.loader = new PIXI.Loader()
this.loader.add('hamster', 'https://cdn-icons-png.flaticon.com/512/196/196817.png')
this.loader.load((loader, resources) => {
...
Better approach is to load image (resource) once at beginning and then just reuse what is loaded in memory:
constructor()
{
...
this.markerTexture = null;
this._loadPixiResources();
}
...
_loadPixiResources()
{
this.loader = new PIXI.Loader()
this.loader.add('hamster', 'https://cdn-icons-png.flaticon.com/512/196/196817.png')
this.loader.load((loader, resources) => {
this.markerTexture = resources.hamster.texture;
})
}
...
addImage2()
{
...
let marker = new PIXI.Sprite(this.markerTexture);
Second problem: size of hamsters :)
Scale was set like this:
marker.scale.set(1 / scale)
Which was too big - so changed it to:
// affects size of hamsters:
this.scaleFactor = 0.05;
...
marker.scale.set(this.scaleFactor / scale);
Scale of hamsters (not triangles!) is now updated when zoom changes - so when user uses mouse scroll wheel etc.
Third problem: too many layers in pixiOverlay
Previously on each click on Add Image 2 or Add Triangle button there was added new pixiContainer and new pixiOverlay which was added as new layer: this.imagesLayer.addLayer(pixiOverlay);
New version is a bit simplified: there is only one pixiContainer and one pixiOverlay created at beginning:
constructor()
{
...
// Create one Pixi container for pixiOverlay in which we will keep hamsters and triangles:
this.pixiContainer = new PIXI.Container();
let prevZoom;
// Create one pixiOverlay:
this.pixiOverlay = L.pixiOverlay((utils, data) => {
...
}, this.pixiContainer)
this.imagesLayer.addLayer(this.pixiOverlay);
}
this.pixiOverlay is added as one layer
then in rest of program we reuse this.pixiOverlay
also we reuse this.pixiContainer because it is returned from utils - see:
let container = utils.getContainer() // <-- this is our "this.pixiContainer"
...
container.addChild(marker)
renderer.render(container)
Bonus: Triangles
Now you can add many triangles - one per each click.
Note: triangles do not change scale - this is a difference compared to hamsters.
I have a problem when I go to paint the PIXI.Text in the cursor position.
This is the simple demo to reproduce the problem, when you go over the node with the cursor I paint the text, in this case, "#author vincenzopalazzo" but I want the position on the node, so I think for resolving the problem I have got the solution I must set the position of the mouse.
But I don't have an idea got get this position, so this is an example to reproduce the problem with PIXI
//setup Pixi renderer
var renderer = PIXI.autoDetectRenderer(600, 400, {
backgroundColor: 0x000000
});
document.body.appendChild(renderer.view);
// create new stage
var stage = new PIXI.Container();
// create helpful message
var style = {
font: '18px Courier, monospace',
fill: '#ffffff'
};
// create graphic object called circle then draw a circle on it
var circle = new PIXI.Graphics();
circle.lineStyle(5, 0xFFFFFF, 1);
circle.beginFill(0x0000FF, 1);
circle.drawCircle(150, 150, 100);
circle.endFill();
circle.alpha = 0.5;
stage.addChild(circle);
// designate circle as being interactive so it handles events
circle.interactive = true;
// create hit area, needed for interactivity
circle.hitArea = new PIXI.Circle(150, 150, 100);
// make circle non-transparent when mouse is over it
circle.mouseover = function(events) {
var message = new PIXI.Text('Hover your mouse over the circle to see the effect.', style);
message.x = events.clientX;
message.y = events.clientY;
circle.message = message;
circle.addChild(message);
}
// make circle half-transparent when mouse leaves
circle.mouseout = function(mouseData) {
this.alpha = 0.5;
circle.removeChild(circle.message);
delete circle.message;
}
// start animating
animate();
function animate() {
requestAnimationFrame(animate);
// render the root container
renderer.render(stage);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/5.1.4/pixi.js"></script>
This is my real code
module.exports = function (animatedNode, ctx) {
ctx.on('hover', function(animatedNode, ctx){
let x = animatedNode.pos.x;
let y = - animatedNode.pos.y / 2;
if(animatedNode.label === undefined){
animatedNode.label = new PIXI.Text('#author vincenzopalazzo', { fontFamily: "Arial", fontSize: "20px" , fill: 0x000000} );
animatedNode.label.x = x;
animatedNode.label.y = y + animatedNode.width/2;
ctx.addChild(animatedNode.label);
}else{
animatedNode.label.x = x;
animatedNode.label.y = y + animatedNode.width/2;
}
});
ctx.on('unhover', function(animatedNode, ctx){
ctx.removeChild(animatedNode.label);
delete animatedNode.label;
});
ctx.mouseover = function() {
console.debug('I\'call the hover events');
this.fire('hover', animatedNode, ctx);
}
ctx.mouseout = function() {
console.debug('I\'call the unhover events');
this.fire('unhover', animatedNode, ctx);
}
}
I'm using the ngraph.events on the ctx (it is the PIXI graphics) object, the method on and fire is the module nghraph.events
In your example code (first snippet) the "moseover" handler should be changed from:
// make circle non-transparent when mouse is over it
circle.mouseover = function(events) {
var message = new PIXI.Text('Hover your mouse over the circle to see the effect.', style);
message.x = events.clientX;
message.y = events.clientY;
circle.message = message;
circle.addChild(message);
}
to:
// make circle non-transparent when mouse is over it
circle.on('mouseover', function(event) {
// console.log('mouse is over the circle');
// console.log(event); // see in (for example in Chrome Devtools console) what is inside this variable
var message = new PIXI.Text('Hover your mouse over the circle to see the effect.', style);
// By looking at what "console.log(event)" shows we can see that instead of:
// message.x = events.clientX;
// message.y = events.clientY;
// should be:
message.x = event.data.global.x;
message.y = event.data.global.y;
circle.message = message;
circle.addChild(message);
});
To understand it more you can uncomment the "console.log" lines to observe it in your browser devtools console.
Then we also need to handle 'mouseover' event like this:
circle.on('mousemove',function (event) {
if (!circle.message) {
return;
}
var newPosition = event.data.getLocalPosition(this.parent);
circle.message.x = newPosition.x;
circle.message.y = newPosition.y;
});
so whole runnable example will be like this:
//setup Pixi renderer
var renderer = PIXI.autoDetectRenderer(600, 400, {
backgroundColor: 0x000000
});
document.body.appendChild(renderer.view);
// create new stage
var stage = new PIXI.Container();
// create helpful message
var style = {
font: '18px Courier, monospace',
fill: '#ffffff'
};
// create graphic object called circle then draw a circle on it
var circle = new PIXI.Graphics();
circle.lineStyle(5, 0xFFFFFF, 1);
circle.beginFill(0x0000FF, 1);
circle.drawCircle(150, 150, 100);
circle.endFill();
circle.alpha = 0.5;
stage.addChild(circle);
// designate circle as being interactive so it handles events
circle.interactive = true;
// create hit area, needed for interactivity
circle.hitArea = new PIXI.Circle(150, 150, 100);
// make circle non-transparent when mouse is over it
circle.on('mouseover', function(event) {
// console.log('mouse is over the circle');
// console.log(event); // see in (for example in Chrome Devtools console) what is inside this variable
var message = new PIXI.Text('Hover your mouse over the circle to see the effect.', style);
// By looking at what "console.log(event)" shows we can see that instead of:
// message.x = events.clientX;
// message.y = events.clientY;
// should be:
message.x = event.data.global.x;
message.y = event.data.global.y;
circle.message = message;
circle.addChild(message);
});
circle.on('mousemove',function (event) {
if (!circle.message) {
return;
}
var newPosition = event.data.getLocalPosition(this.parent);
circle.message.x = newPosition.x;
circle.message.y = newPosition.y;
});
// make circle half-transparent when mouse leaves
circle.mouseout = function(mouseData) {
this.alpha = 0.5;
circle.removeChild(circle.message);
delete circle.message;
}
// start animating
animate();
function animate() {
requestAnimationFrame(animate);
// render the root container
renderer.render(stage);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/5.1.4/pixi.js"></script>
Please also see:
"Interaction/Dragging" Pixi.js demo (with source code): https://pixijs.io/examples/#/interaction/dragging.js
tutorial "Rotate towards mouse and shoot in that direction" by Igor Neuhold : http://proclive.io/shooting-tutorial/
Pixi.js API reference:
https://pixijs.download/dev/docs/PIXI.DisplayObject.html#event:mousemove
https://pixijs.download/dev/docs/PIXI.interaction.InteractionEvent.html
https://pixijs.download/dev/docs/PIXI.interaction.InteractionData.html