Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to represent irregular polygon as sprite? #38

Closed
wadehenning opened this issue Sep 5, 2018 · 16 comments
Closed

How to represent irregular polygon as sprite? #38

wadehenning opened this issue Sep 5, 2018 · 16 comments
Assignees

Comments

@wadehenning
Copy link

wadehenning commented Sep 5, 2018

Thank you for this beautiful repo. How do I replace the 3D Text nodes with irregular shapes? For example, my shapes might look something California in this example.
https://threejs.org/examples/#webgl_geometry_shapes

EDIT/ SOLUTION: @vasturiano was extremely patient and helpful as I got up to speed on canvas and three.js; the rest of this post is my journey. Here is my bare-bones solution to my original question:

import { ForceGraph3D } from 'react-force-graph';
import { genRandomTree } from './random-data';
var THREE = require('three');

export default class SpriteGraph extends React.Component {

    generateMaterial = () => {  
      //create a canvas   
        let canvas = document.createElement('canvas');
        canvas.width = 160;
        canvas.height = 160;
        let ctx = canvas.getContext('2d');

       //draw a shape
        let PI2 = Math.PI * 2;
        ctx.beginPath();
        ctx.fillStyle = 'blue';
        ctx.arc(canvas.width/2, canvas.height/2, 80, 0, PI2, true);
        ctx.fill();

        //return a material
        let texture = new THREE.CanvasTexture(canvas);      
        let material = new THREE.SpriteMaterial({map: texture})      
        return material
    }

    render() {
        const data = genRandomTree(100);        
        return <ForceGraph3D           
            graphData={data}
            nodeLabel="id"
            nodeThreeObject={({ id }) => {
                let sprite = new THREE.Sprite(this.generateMaterial())
                sprite.scale.set(30,30,1)
                return sprite
            }}
        />;
    }
}
@vasturiano
Copy link
Owner

@wadehenning thanks for reaching out. You can set any custom geometries for the nodes, using the nodeThreeObject prop.

Here's an example with custom geometries:
https://vasturiano.github.io/react-force-graph/example/custom-node-shape/index-three.html
https://github.com/vasturiano/react-force-graph/blob/master/example/custom-node-shape/index-three.html

@wadehenning
Copy link
Author

Thank you for focusing me on the nodeThreeObject. I think I am close to being able to plot a 2D curve (ignoring the Sprite part for now), but I am getting an error: TypeError: shape.extractPoints is not a function. Any thoughts?

My component code:
<ForceGraph3D
graphData={data}
nodeThreeObject={ ({id}) => new THREE.Mesh(
[
new THREE.ShapeBufferGeometry( {shape} )
][id%1],
new THREE.MeshPhongMaterial(
{ color: 0xf08000} )
)}
/>;

My shape:
// California
var californiaPts = [];
californiaPts.push( new THREE.Vector2( 610, 320 ) );
californiaPts.push( new THREE.Vector2( 450, 300 ) );
..................................
californiaPts.push( new THREE.Vector2( 610, 320 ) );
for( var i = 0; i < californiaPts.length; i ++ ) californiaPts[ i ].multiplyScalar( 0.25 );
var shape = new THREE.Shape( californiaPts );

@wadehenning
Copy link
Author

I think I'm doing something wrong with sending the shape to the component

@wadehenning
Copy link
Author

I have been working to get a simple custom shape plotted and I am getting the error: TypeError: Cannot read property 'extractPoints' of undefined.

Does my simple code look correct?

import React, { Component } from 'react';
import { ForceGraph2D, ForceGraph3D, ForceGraphVR } from 'react-force-graph';
import * as THREE from 'three';

export default class FocusGraph extends React.Component {
render() {
const {data, shape} = this.props;
console.log(shape)
return <ForceGraph3D
graphData={data}
nodeThreeObject={ ( {shape} ) => new THREE.Mesh(
[
new THREE.ShapeBufferGeometry( shape )
],
new THREE.MeshPhongMaterial(
{ color: 0xf08000,
side: THREE.DoubleSide} )
)}
/>;
}
}

My shape looks like this:
// California
var californiaPts = [];
californiaPts.push( new THREE.Vector2( 610, 320 ) );
californiaPts.push( new THREE.Vector2( 450, 300 ) );
...
californiaPts.push( new THREE.Vector2( 610, 320 ) );
for( var i = 0; i < californiaPts.length; i ++ ) californiaPts[ i ].multiplyScalar( 0.25 );
var shape = new THREE.Shape( californiaPts );

Thanks

@wadehenning
Copy link
Author

I'm sure I'll figure it out.

@wadehenning
Copy link
Author

I have been able to get all of your examples working, and I have been able to add custom shapes. But I am still having trouble with custom sprites of shapes. My sprite code seems to generate sprites, but I get an error message:
Uncaught TypeError: Cannot read property 'x' of undefined
at WebGLSpriteRenderer.render (aframe-master.js:12357)
at WebGLRenderer.render (aframe-master.js:27568)
at Function.tick (three-render-objects.module.js:100)
at Function.r.(:3000/anonymous function) [as tick] (http://localhost:3000/static/js/bundle.js:161238:1813)
at animate (3d-force-graph.module.js:391)

I hope that my code is close enough for your review.

First I have a simple React Component, followed by short code to make my sprite.

export default class SpriteCurves extends React.Component {
render() {
const {data, shape, geometry} = this.props;
return <ForceGraph3D
graphData={data}
nodeAutoColorBy="group"
nodeThreeObject={() => {
const sprite = generatesprite(shape);
return sprite;
}}
/>;
}
}

SPRITE CODE:
function generateTexture(shape) {
var canvas = document.createElement( 'canvas' );
canvas.width = 20;
canvas.height = 20;
var context = canvas.getContext( '2d' );
var canvasShape = shape;
return canvas;
};

export function generatesprite(shape){
var texture = new THREE.Texture( generateTexture(shape) );
texture.needsUpdate = true; // important!
var material = new THREE.SpriteMaterial( { map: texture, color: 0xffffff } );
material.sizeAttenuation = false;
var sprite = new THREE.Sprite( material );
console.log(sprite);
return sprite;
};

@wadehenning wadehenning reopened this Sep 27, 2018
@vasturiano
Copy link
Owner

Hi @wadehenning could you make an example online, on CodePen or similar so that it's easier to debug?

For now, I'd check whether:

  1. Does the example work fine if you don't use the nodeThreeObject prop?
  2. Does generatesprite always return a non-null Three Object3D?

@wadehenning
Copy link
Author

wadehenning commented Oct 1, 2018

Hi @vasturiano .

  1. Yes- the example works fine without the nodeThreeObject.
  2. generatesprite always returns a Sprite object created using a canvas and context path. Is this appropriate?

I'm working on a codesandbox.

@wadehenning
Copy link
Author

wadehenning commented Oct 19, 2018

Hi @vasturiano.
Your incisive questions helped me figure out the error I was getting and I think I am getting close. I am now focusing on simply getting my irregular polygons to plot as contexts in 2D. The links plot perfectly and I get no errors, but I also get no nodes at all. Any thoughts on this code? CTX is an array of contexts generated from my polygons. I have tried enclosing {CTX} in curly braces in the nodeCanvasObject with no change. Maybe I am just missing something regarding how to pass data into the component.

    render() {                
      const {Shapes, data, CTX} = this.props;        
      console.log(CTX);
      return <ForceGraph2D    
          graphData={data}  
          nodeLabel="id"  
          nodeCanvasObject={ CTX => {         
                 () => {  CTX.fill(); }
          } }           
    />;
    }
  }

@vasturiano
Copy link
Owner

@wadehenning the signature of nodeCanvasObject is

.nodeCanvasObject((<nodeData>, <canvas context>, <current global scale>) => { /* ... do something with the context for representing this node */ })

In your code above, you're not unpacking the function signature correctly, it should be (node, ctx) => {...}.

Also you should do some drawing operations on the context, otherwise ctx.fill() has nothing to fill.

Here's an example of using the nodeCanvasObject that may be of help:
https://github.com/vasturiano/force-graph/blob/master/example/custom-node-shape/index.html

@vasturiano vasturiano self-assigned this Oct 20, 2018
@wadehenning
Copy link
Author

@vasturiano in the line below, I don't understand where the values for the input parameters come from. What is the value of ctx and where is it coming from? Where are id, x,, y coming from when you don't call gData until later? So in my code, I pre-draw the ctx polygons, save it in state and send it through props as array CTX.

.nodeCanvasObject(({ id, x, y }, ctx) => {

If I simply add my nodes data to the function as an input parameter it does not help, so I clearly need something different.
Thank you!

@vasturiano
Copy link
Owner

The ctx is the canvas context onto which you should run the drawing commands (like fillRect, beginPath, etc). You can not pre-draw on a separate canvas and then apply it to the chart canvas.

The { id, x, y} is just a way to unpack the node's data. You can simply replace that with:

.nodeCanvasObject((nodeData, ctx) => { ... }

The nodeData is useful to let you define how a particular node should look like. For example, you may want node id = 1 to be drawn red, and id = 2 to be drawn green. You can do that by writing your drawing logic based on the nodeData attributes.

@wadehenning
Copy link
Author

Hi @vasturiano
I would like to generate sprites from irregular polygons in a ForceGraph3D component. The Sprite requires a ctx, ctx only comes from a nodeCanvasObject, but ForceGraph3D requires a nodeThreeObject. Do I need to define a canvas inside the component myself? Also, what do you think about how I try and create a Sprite here in my nodeThreeObject? For me, this 3d-force-sprite component is the whole purpose; everything else is just trying to learn the API. Thank you.

export default class SpriteGraph extends React.Component {

    componentDidMount(){  
        this.fg.d3Force('link').distance(links => links.area_dist)
        .iterations(1);
    }    

    generateTexture = (ctx, points, color) => {
            ctx.fillStyle = color;               
            ctx.beginPath(); 
            ctx.moveTo(points[0][0],points[0][1]); 
            for(let i=1; i<points.length;i++){
                ctx.lineTo(points[i][0], points[i][1]); 
            } 
            ctx.fill();
            return ctx;
        }

    render() {
       const data = this.props.data;   
       const getColor = n => '#' + ((n * 1234567) % Math.pow(2, 24)).toString(16).padStart(6, '0');
        return <ForceGraph3D   
        ref={el => { this.fg = el; }}
            d3Force = { ('link', "distance")}
            cooldownTime={90000}        
            graphData={data}
            nodeLabel="id"
            nodeThreeObject={({ id, points }, ctx) => new THREE.Sprite(
                new THREE.SpriteMaterial(
                new THREE.Texture(this.generateTexture(ctx, points, getColor(id)))                  
                ),               
            )}
           
        />;
    }
}


@vasturiano
Copy link
Owner

@wadehenning you should create a new canvas context as part of the operation of generating a THREE object. Remember that this canvas is not attached to the DOM, it serves only the purpose of the sprite construction, so you don't need to get it from or link it with the 3d graph itself.

@wadehenning
Copy link
Author

wadehenning commented Nov 22, 2018

Hi @vasturiano
I am generating random data and trying to plot a sprite with a circle at each node. I do get tiny squares at each node; which to me indicates that a Sprite is being created, but there is a fail somewhere.

Here is a link to a code sandbox where the preview does not work. I think this is related to the force-graph package (it did preview intermittently when I first copied in the example Custom Node Geometries code) , and maybe you have some experience getting these things to render better in these environments.

@wadehenning
Copy link
Author

@vasturiano , I'm sorry that you've had to walk me through this sprite example-- this is the first time I have ever worked with three.js or canvas. What I have learned here has helped me in other components that require canvas, and I appreciate your time.

I don't know if you have had a chance to look at the code sandbox I put up- even though the preview does not work, at least the code is all on one page and it is a trivial example. Here is what my output looks like. It is making some sort of tiny sprite node, but does not include my circles.
spritegraph

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants