Draw a pointer on the edge of the screen that points to the target object behind tha back (invisible on the screen by the camera) - aframe

Live example: https://aframe-aim-component.glitch.me/
Try in Glitch: https://glitch.com/edit/#!/aframe-aim-component
We will see a blue line when we execute the sample code. It runs from a point 1 meter in front of the camera, to the position point of the red cube, which is behind and slightly away from the camera at the start. If we rotate the camera with the mouse to the left, we will turn to the red cube and the blue line will be drawn to the cube.
This blue bar is just an experiment to implement such an idea:
Draw an invisible line from a point in front of the camera, to the target object.
Find the coordinate on the device screen where the line intersects with the screen boundary.
Find the angle of intersection with the screen of this line.
Use HTML to display an arrow element on top of the canvas and rotate it in the direction of the invisible line.
Thanks to Piotr Adam Milewski for help in rendering the line (AFRAME: Coordinates of the fixed point in front of the camera)
As a result, the following behavior is expected: If the target object, with the "aim" component, is behind the player (the invisible line crosses the screen boundary), then an arrow is displayed on the screen boundary, indicating to the player in which direction he needs to turn to see the object.
I'm new to 3D and don't understand a lot of things yet. Please help me to implement a directional arrow pointing to the object.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
<script>
AFRAME.registerComponent("aim", {
init: function() {
this._matAimRay = new THREE.LineBasicMaterial( { color: 0x0000ff } );
this._frustum = new THREE.Frustum();
},
play: function() {
this._camera = this.el.sceneEl.camera;
},
pause: function() {
this._camera = null;
},
remove: function () {
this.el.sceneEl.removeChild(this._pointerEntity);
},
tick: function() {
if (!!this._camera) {
if (!this._checkIsAtScreen(this._camera, this.el.object3D.position)) {
if (!this._pointerEntity) {
//create entity for pointer line
let pointerEntity = document.createElement("a-entity");
this._pointerEntity = pointerEntity;
this.el.sceneEl.appendChild(pointerEntity);
}
let posViewer = new THREE.Vector3(0, 0, -100);
this._camera.localToWorld(posViewer);
const points = [];
points.push( posViewer, this.el.object3D.position );
const geometry = new THREE.BufferGeometry().setFromPoints( points );
const line = new THREE.Line( geometry, this._matAimRay );
this._pointerEntity.setObject3D("mesh", line );
} else if (!!this._pointerEntity) {
this.el.sceneEl.removeChild(this._pointerEntity);
this._pointerEntity = null;
}
}
},
_checkIsAtScreen(camera, pointVector3) {
camera.updateMatrix();
camera.updateMatrixWorld();
this._frustum.setFromProjectionMatrix(new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse));
return this._frustum.containsPoint(pointVector3);
}
});
</script>
<title>Document</title>
</head>
<body>
<a-scene>
<a-entity camera look-controls position="0 0 0"></a-entity>
<a-box aim position="-5 0 5" color="red"></a-box>
</a-scene>
</body>
</html>
UPDATE:
I updated the example code:
The beam to the target object is visible only when the object is not visible to the camera. The same way the pointer (2d compass) will be hidden from the user screen
Moved the point in front of the camera from 1 meter to 100 meters then, so that there is no effect when the beam is interrupted in the edge of the screen when the object is directly behind the camera. With more distance, the chance of this decreasing.
UPDATE 2 (working (bad) example)!!!
There is progress in my research. I don't fully understand how :) But it's starting to work. The problem is in the hautticity of determining the intersection of the imaginary line with furstum and in filtering the valid point, which makes the arrows move jerkily. If you set isValidScreenCoordinates == true, the arrows begin to move smoothly, but there are strange disappearances in the corners in Y...
Now there is something to build on, I will be very happy if you point out my mistakes in the study and contribute to it, to make the movement of arrows smoothly and accurately :))
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
.aim-arrow {
position: absolute;
z-index: 100000;
font-size: 50px;
opacity: 0;
visibility: hidden;
transition: opacity 0ms ease;
}
.aim-arrow.visible {
opacity: 1;
visibility: visible;
transition: opacity 1050ms ease;
}
</style>
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
<script>
AFRAME.registerComponent("aim", {
schema: {
color: {type:'color', default: 'orange'}
},
init: function() {
this._frustum = new THREE.Frustum();
let pointerEl = document.createElement("div");
pointerEl.textContent = `◄`;
pointerEl.setAttribute("class", "aim-arrow");
this._pointerEl = pointerEl;
document.querySelector("body").appendChild(this._pointerEl);
},
update: function(oldData) {
this._pointerEl.style.color = this.data.color;
},
play: function() {
this._camera = this.el.sceneEl.camera;
},
pause: function() {
this._camera = null;
},
remove: function () {
this._pointerEl.parentElement.removeChild(this._pointerEl);
},
tick: function() {
if (!!this._camera) {
if (!this._checkIsAtScreen(this._camera, this.el.object3D.position)) {
this._containerWidth = window.innerWidth;
this._containerHeight = window.innerHeight;
let posViewer = new THREE.Vector3(0, 0, (this._camera.near + this._camera.far / 2) * -1);
this._camera.localToWorld(posViewer);
//find intersections
let line3 = new THREE.Line3();
line3.set(posViewer, this.el.object3D.position);
let intersectionPoint = new THREE.Vector3();
let isValidScreenCoordinates = false;
for (let i = 0; i < this._frustum.planes.length; i++) {
this._frustum.planes[i].intersectLine(line3, intersectionPoint);
if(!!intersectionPoint) {
let screenCoordinate = this._vector3ToScreenXY(this._camera, intersectionPoint, this._containerWidth, this._containerHeight);
if (!!screenCoordinate) {
isValidScreenCoordinates = (screenCoordinate.x >= 0) && (screenCoordinate.y >= 0) && (screenCoordinate.x <= this._containerWidth) && (screenCoordinate.y <= this._containerHeight);
if (isValidScreenCoordinates) {
this._placePointer(screenCoordinate, this._containerWidth, this._containerHeight);
//display pointer
this._pointerEl.classList.toggle("visible", true);
} else {
console.log('not valid', screenCoordinate);
}
}
}
}
} else {
//hide pointer
this._pointerEl.classList.toggle("visible", false);
}
}
},
_checkIsAtScreen(camera, pointVector3) {
this._frustum.setFromProjectionMatrix(new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse));
return this._frustum.containsPoint(pointVector3);
},
_vector3ToScreenXY(camera, vector3, containerWidth, containerHeight) {
let result = null;
let pos = vector3.clone();
let projScreenMat = new THREE.Matrix4();
projScreenMat.multiply( camera.projectionMatrix, camera.matrixWorldInverse );
projScreenMat.multiplyVector3( pos );
let x = ( pos.x + 1 ) * containerWidth / 2;
let y = ( - pos.y + 1) * containerHeight / 2;
if (!isNaN(x) && !isNaN(y)) {
result = { x, y };
}
return result;
},
_findAimPointerAngle(pointerXYCoordinate, containerWidth, containerHeight) {
let cx = pointerXYCoordinate.x;
let cy = pointerXYCoordinate.y;
let ex = containerWidth / 2;
let ey = containerHeight / 2;
let dy = ey - cy;
let dx = ex - cx;
let theta = Math.atan2(dy, dx); // range (-PI, PI]
theta *= 180 / Math.PI; // rads to degs, range (-180, 180]
if (theta < 0) theta = 360 + theta; // range [0, 360)
return theta;
},
_placePointer(screenCoordinate, containerWidth, containerHeight) {
let angle = this._findAimPointerAngle(screenCoordinate, containerWidth, containerHeight);
this._pointerEl.style.left = `${screenCoordinate.x}px`;
this._pointerEl.style.top = `${screenCoordinate.y}px`;
this._pointerEl.style.transformOrigin = "0% 0%";
this._pointerEl.style.transform = `rotate(${angle}deg) translate(0%, -50%)`;
}
});
</script>
<title>Document</title>
</head>
<body>
<a-scene>
<a-entity camera look-controls="wasd-controls: false; pointerLockEnabled: true" wasd-controls position="0 0 0"></a-entity>
<a-box aim="color: red" position="-5 0 5" color="red"></a-box>
<a-box aim="color: blue" position="5 0 5" color="blue"></a-box>
</a-scene>
</body>
</html>

Related

D3 outdated links still visible

What I got:
I got a D3 forced graph, with two different link types. I do visualize those two types differently. need as a simple line and uses dashed. To do so I got two different CSS classes for the links and simply switch the type if clicked. To finally visualize this changes I call the main initialize() function again.
Whats the problem:
As soon as I click on one of those links and switch the type, the outdated lines are still visible. I miss the point how to avoid such behavior? How can I make sure, that the outdated lines are gone? I appreciate any hint.
Update:
I added svg.selectAll("line").remove() before the re-init. But I doubt its best practice, further sometimes the lines just disappear completely.
var graph = {
"nodes": [
{
"id": 1
},
{
"id": 2
},
{
"id": 3
}
],
"links": [
{
"source": 1,
"target": 2,
"type": "uses"
},
{
"source": 2,
"target": 3,
"type": "needs"
},
{
"source": 3,
"target": 1,
"type": "needs"
}
]
}
var svg = d3.select("svg")
.attr("width", window.innerWidth)
.attr("height", window.innerHeight)
var force = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id
}).distance(80))
.force("charge", d3.forceManyBody().strength(-100))
.force("center", d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2))
.force("collision", d3.forceCollide().radius(90))
initialize()
function initialize() {
link = svg.selectAll(".link")
.data(graph.links)
.join("line")
//.attr("class", "link")
.attr("class", function (d) {
if (d.type === "uses") {
return "uses"
} else {
return "needs"
}
})
.on("dblclick", function (event, d) {
if (d.type === "uses") {
d.type = "needs"
} else if (d.type === "needs") {
d.type = "uses"
}
svg.selectAll("line").remove()
initialize()
})
node = svg.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
node.selectAll("circle")
.data(graph.nodes)
.join("circle")
.attr("r", 30)
.style("fill", "whitesmoke")
force
.nodes(graph.nodes)
.on("tick", ticked);
force
.force("link")
.links(graph.links)
}
function ticked() {
// update link positions
link
.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
// update node positions
node
.attr("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")";
});
}
function dragStarted(event, d) {
if (!event.active) force.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
PosX = d.x
PosY = d.y
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) force.alphaTarget(0);
d.fx = undefined;
d.fy = undefined;
}
body {
height: 100%;
background: #e6e7ee;
overflow: hidden;
margin: 0px;
}
line {
stroke-width: 6px;
}
line.uses {
stroke: grey;
stroke-dasharray: 5;
}
line.needs {
stroke: black;
}
line:hover {
stroke: goldenrod;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- d3.js framework -->
<script src="https://d3js.org/d3.v6.js"></script>
</head>
<body>
<svg id="svg"></svg>
</body>
</html>
In the segment:
link = svg.selectAll(".link")
.data(graph.links)
.join("line")
//.attr("class", "link")
.attr("class", function (d) {
if (d.type === "uses") {
return "uses"
} else {
return "needs"
}
})
The selection is selecting the link class, but the lines actually have uses or needs as classes. You can instead select the previous uses and needs classes:
link = svg.selectAll("line.uses,line.needs")
This will make the .join() remove the unused lines of those classes.

Does Vis Timeline tooltips work timeline - groups-ordering?

I'm trying to get tooltips working with Timeline | Groups ordering.
I'm getting the error:
Uncaught Error: No container provided
at Timeline.Core._create (vis.js:9748)
at new Timeline (vis.js:40685)
Are there any examples of Timeline | Groups ordering with tooltips?
Looking at http://visjs.org/examples/timeline/items/tooltip.html
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Groups ordering</title>
<style>
body, html {
font-family: arial, sans-serif;
font-size: 11pt;
}
#visualization {
box-sizing: border-box;
position: relative;
width: 100%;
height: 300px;
}
</style>
<script src="./dist/moment.min.js"></script>
<script src="./dist/vis.js"></script>
<link href="./dist/vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
</head>
<body>
<h1> Oracle Database </h1>
<div id="tooltips-follow">
<div class="menu">
<input type="image" src="dist/img/network/plus.png" id="zoomIn" value="Zoom in"/>
<input type="image" src="dist/img/network/minus.png" id="zoomOut" value="Zoom out"/>
<input type="image" src="dist/img/network/leftArrow.png" id="moveLeft" value="Move left"/>
<input type="image" src="dist/img/network/rightArrow.png" id="moveRight" value="Move right"/>
</div>
</div>
<script>
var groups = new vis.DataSet([
{id:0, content: "Database Roadmap", value: 0},
...
]);
// create a dataset with items
// note that months are zero-based in the JavaScript Date object, so month 3 is April
var items = new vis.DataSet([
{id:1, group:0, content: "Full", className: "Full", start: new Date(2018, 01, 01), end: new Date(2022, 06, 30), title: 'Start Date2010, 07, 01<br> End Date2010, 07, 01'},
...
]);
// create visualization
var container = document.getElementById('tooltips-follow');
var options = {
// option groupOrder can be a property name or a sort function
// the sort function must compare two groups and return a value
// > 0 when a > b
// < 0 when a < b
// 0 when a == b
groupOrder: function (a, b) {
return a.value - b.value;
},
start: new Date(),
end: new Date(1000*60*60*36864 + (new Date()).valueOf()),
stack: false,
editable: false,
clickToUse: true
};
var timeline = new vis.Timeline(container);
timeline.setOptions(options);
timeline.setGroups(groups);
timeline.setItems(items);
function move (percentage) {
var range = timeline.getWindow();
var interval = range.end - range.start;
timeline.setWindow({
start: range.start.valueOf() - interval * percentage,
end: range.end.valueOf() - interval * percentage
});
}
// attach events to the navigation buttons
document.getElementById('zoomIn').onclick = function () { timeline.zoomIn( 0.2); };
document.getElementById('zoomOut').onclick = function () { timeline.zoomOut( 0.2); };
document.getElementById('moveLeft').onclick = function () { move( 0.2); };
document.getElementById('moveRight').onclick = function () { move(-0.2); };
// Follow options
var follow_options = {
tooltip: {
followMouse: true
}
};
var timelineFollow = new vis.Timeline(document.getElementById('tooltips-follow'),
items, follow_options);
</script>
</body>
</html>
So it works by just adding the title tag and using the follow option does not work. Thanks

Rendering from two cameras at the same time in A-Frame

the recent v0.3.0 blog post mentions WebVR 1.0 support allowing "us to have different content on the desktop display than the headset, opening the door for asynchronous gameplay and spectator modes." This is precisely what I'm trying to get working. I'm looking to have one camera in the scene represent the viewpoint of the HMD and a secondary camera represent a spectator of the same scene and render that view to a canvas on the same webpage. 0.3.0 removes the ability to render a-scene to a specific canvas in favor of embedded component. Any thoughts on how to accomplish two cameras rendering a single scene simultaneously?
My intention is to have a the desktop display show what a user is doing from a different perspective. My end goal is to be able to build a mixed reality green screen component.
While there may be a better or cleaner way to do this in the future, I was able to get a second camera rendering by looking at examples of how this is done in the THREE.js world.
I add a component to a non-active camera called spectator. in the init function I set up a new renderer and attach to div outside the scene to create a new canvas. I then call the render method inside the tick() part of the lifecycle.
I have not worked out how to isolate the movement of this camera yet. The default look controls of the 0.3.0 aframe scene still control both camera
Source code:
https://gist.github.com/derickson/334a48eb1f53f6891c59a2c137c180fa
I've created a set of components that can help with this. https://github.com/diarmidmackenzie/aframe-multi-camera
Here's an example showing usage with A-Frame 1.2.0 to display the main camera on the left half of the screen, and a secondary camera on the right half.
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/diarmidmackenzie/aframe-multi-camera#latest/src/multi-camera.min.js"></script>
</head>
<body>
<div>
<a-scene>
<a-entity camera look-controls wasd-controls position="0 1.6 0">
<!-- first secondary camera is a child of the main camera, so that it always has the same position / rotation -->
<!-- replace main camera (since main camera is rendered across the whole screen, which we don't want) -->
<a-entity
id="camera1"
secondary-camera="outputElement:#viewport1;sequence: replace"
>
</a-entity>
</a-entity>
<!-- PUT YOUR SCENE CONTENT HERE-->
<!-- position of 2nd secondary camera-->
<a-entity
id="camera2"
secondary-camera="outputElement:#viewport2"
position="8 1.6 -6"
rotation="0 90 0"
>
</a-entity>
</a-scene>
</div>
<!-- standard HTML to contrl layout of the two viewports-->
<div style="width: 100%; height:100%; display: flex">
<div id="viewport1" style="width: 50%; height:100%"></div>
<div id="viewport2" style="width: 50%; height:100%"></div>
</div>
</body>
</html>
Also here as a glitch: https://glitch.com/edit/#!/recondite-polar-hyssop
It's also been suggested that I post the entire source code for the multi-camera component here.
Here it is...
/* System that supports capture of the the main A-Frame render() call
by add-render-call */
AFRAME.registerSystem('add-render-call', {
init() {
this.render = this.render.bind(this);
this.originalRender = this.el.sceneEl.renderer.render;
this.el.sceneEl.renderer.render = this.render;
this.el.sceneEl.renderer.autoClear = false;
this.preRenderCalls = [];
this.postRenderCalls = [];
this.suppresssDefaultRenderCount = 0;
},
addPreRenderCall(render) {
this.preRenderCalls.push(render)
},
removePreRenderCall(render) {
const index = this.preRenderCalls.indexOf(render);
if (index > -1) {
this.preRenderCalls.splice(index, 1);
}
},
addPostRenderCall(render) {
this.postRenderCalls.push(render)
},
removePostRenderCall(render) {
const index = this.postRenderCalls.indexOf(render);
if (index > -1) {
this.postRenderCalls.splice(index, 1);
}
else {
console.warn("Unexpected failure to remove render call")
}
},
suppressOriginalRender() {
this.suppresssDefaultRenderCount++;
},
unsuppressOriginalRender() {
this.suppresssDefaultRenderCount--;
if (this.suppresssDefaultRenderCount < 0) {
console.warn("Unexpected unsuppression of original render")
this.suppresssDefaultRenderCount = 0;
}
},
render(scene, camera) {
renderer = this.el.sceneEl.renderer
// set up THREE.js stats to correctly count across all render calls.
renderer.info.autoReset = false;
renderer.info.reset();
this.preRenderCalls.forEach((f) => f());
if (this.suppresssDefaultRenderCount <= 0) {
this.originalRender.call(renderer, scene, camera)
}
this.postRenderCalls.forEach((f) => f());
}
});
/* Component that captures the main A-Frame render() call
and adds an additional render call.
Must specify an entity and component that expose a function call render(). */
AFRAME.registerComponent('add-render-call', {
multiple: true,
schema: {
entity: {type: 'selector'},
componentName: {type: 'string'},
sequence: {type: 'string', oneOf: ['before', 'after', 'replace'], default: 'after'}
},
init() {
this.invokeRender = this.invokeRender.bind(this);
},
update(oldData) {
// first clean up any old settings.
this.removeSettings(oldData)
// now add new settings.
if (this.data.sequence === "before") {
this.system.addPreRenderCall(this.invokeRender)
}
if (this.data.sequence === "replace") {
this.system.suppressOriginalRender()
}
if (this.data.sequence === "after" ||
this.data.sequence === "replace")
{
this.system.addPostRenderCall(this.invokeRender)
}
},
remove() {
this.removeSettings(this.data)
},
removeSettings(data) {
if (data.sequence === "before") {
this.system.removePreRenderCall(this.invokeRender)
}
if (data.sequence === "replace") {
this.system.unsuppressOriginalRender()
}
if (data.sequence === "after" ||
data.sequence === "replace")
{
this.system.removePostRenderCall(this.invokeRender)
}
},
invokeRender()
{
const componentName = this.data.componentName;
if ((this.data.entity) &&
(this.data.entity.components[componentName])) {
this.data.entity.components[componentName].render(this.el.sceneEl.renderer, this.system.originalRender);
}
}
});
/* Component to set layers via HTML attribute. */
AFRAME.registerComponent('layers', {
schema : {type: 'number', default: 0},
init: function() {
setObjectLayer = function(object, layer) {
if (!object.el ||
!object.el.hasAttribute('keep-default-layer')) {
object.layers.set(layer);
}
object.children.forEach(o => setObjectLayer(o, layer));
}
this.el.addEventListener("loaded", () => {
setObjectLayer(this.el.object3D, this.data);
});
if (this.el.hasAttribute('text')) {
this.el.addEventListener("textfontset", () => {
setObjectLayer(this.el.object3D, this.data);
});
}
}
});
/* This component has code in common with viewpoint-selector-renderer
However it's a completely generic stripped-down version, which
just delivers the 2nd camera function.
i.e. it is missing:
- The positioning of the viewpoint-selector entity.
- The cursor / raycaster elements.
*/
AFRAME.registerComponent('secondary-camera', {
schema: {
output: {type: 'string', oneOf: ['screen', 'plane'], default: 'screen'},
outputElement: {type: 'selector'},
cameraType: {type: 'string', oneOf: ['perspective, orthographic'], default: 'perspective'},
sequence: {type: 'string', oneOf: ['before', 'after', 'replace'], default: 'after'},
quality: {type: 'string', oneOf: ['high, low'], default: 'high'}
},
init() {
if (!this.el.id) {
console.error("No id specified on entity. secondary-camera only works on entities with an id")
}
this.savedViewport = new THREE.Vector4();
this.sceneInfo = this.prepareScene();
this.activeRenderTarget = 0;
// add the render call to the scene
this.el.sceneEl.setAttribute(`add-render-call__${this.el.id}`,
{entity: `#${this.el.id}`,
componentName: "secondary-camera",
sequence: this.data.sequence});
// if there is a cursor on this entity, set it up to read this camera.
if (this.el.hasAttribute('cursor')) {
this.el.setAttribute("cursor", "canvas: user; camera: user");
this.el.addEventListener('loaded', () => {
this.el.components['raycaster'].raycaster.layers.mask = this.el.object3D.layers.mask;
const cursor = this.el.components['cursor'];
cursor.removeEventListeners();
cursor.camera = this.camera;
cursor.canvas = this.data.outputElement;
cursor.canvasBounds = cursor.canvas.getBoundingClientRect();
cursor.addEventListeners();
cursor.updateMouseEventListeners();
});
}
if (this.data.output === 'plane') {
if (!this.data.outputElement.hasLoaded) {
this.data.outputElement.addEventListener("loaded", () => {
this.configureCameraToPlane()
});
} else {
this.configureCameraToPlane()
}
}
},
configureCameraToPlane() {
const object = this.data.outputElement.getObject3D('mesh');
function nearestPowerOf2(n) {
return 1 << 31 - Math.clz32(n);
}
// 2 * nearest power of 2 gives a nice look, but at a perf cost.
const factor = (this.data.quality === 'high') ? 2 : 1;
const width = factor * nearestPowerOf2(window.innerWidth * window.devicePixelRatio);
const height = factor * nearestPowerOf2(window.innerHeight * window.devicePixelRatio);
function newRenderTarget() {
const target = new THREE.WebGLRenderTarget(width,
height,
{
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
stencilBuffer: false,
generateMipmaps: false
});
return target;
}
// We use 2 render targets, and alternate each frame, so that we are
// never rendering to a target that is actually in front of the camera.
this.renderTargets = [newRenderTarget(),
newRenderTarget()]
this.camera.aspect = object.geometry.parameters.width /
object.geometry.parameters.height;
},
remove() {
this.el.sceneEl.removeAttribute(`add-render-call__${this.el.id}`);
if (this.renderTargets) {
this.renderTargets[0].dispose();
this.renderTargets[1].dispose();
}
// "Remove" code does not tidy up adjustments made to cursor component.
// rarely necessary as cursor is typically put in place at the same time
// as the secondary camera, and so will be disposed of at the same time.
},
prepareScene() {
this.scene = this.el.sceneEl.object3D;
const width = 2;
const height = 2;
if (this.data.cameraType === "orthographic") {
this.camera = new THREE.OrthographicCamera( width / - 2, width / 2, height / 2, height / - 2, 1, 1000 );
}
else {
this.camera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000);
}
this.scene.add(this.camera);
return;
},
render(renderer, renderFunction) {
// don't bother rendering to screen in VR mode.
if (this.data.output === "screen" && this.el.sceneEl.is('vr-mode')) return;
var elemRect;
if (this.data.output === "screen") {
const elem = this.data.outputElement;
// get the viewport relative position of this element
elemRect = elem.getBoundingClientRect();
this.camera.aspect = elemRect.width / elemRect.height;
}
// Camera position & layers match this entity.
this.el.object3D.getWorldPosition(this.camera.position);
this.el.object3D.getWorldQuaternion(this.camera.quaternion);
this.camera.layers.mask = this.el.object3D.layers.mask;
this.camera.updateProjectionMatrix();
if (this.data.output === "screen") {
// "bottom" position is relative to the whole viewport, not just the canvas.
// We need to turn this into a distance from the bottom of the canvas.
// We need to consider the header bar above the canvas, and the size of the canvas.
const mainRect = renderer.domElement.getBoundingClientRect();
renderer.getViewport(this.savedViewport);
renderer.setViewport(elemRect.left - mainRect.left,
mainRect.bottom - elemRect.bottom,
elemRect.width,
elemRect.height);
renderFunction.call(renderer, this.scene, this.camera);
renderer.setViewport(this.savedViewport);
}
else {
// target === "plane"
// store off current renderer properties so that they can be restored.
const currentRenderTarget = renderer.getRenderTarget();
const currentXrEnabled = renderer.xr.enabled;
const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
// temporarily override renderer proeperties for rendering to a texture.
renderer.xr.enabled = false; // Avoid camera modification
renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows
const renderTarget = this.renderTargets[this.activeRenderTarget];
renderTarget.texture.encoding = renderer.outputEncoding;
renderer.setRenderTarget(renderTarget);
renderer.state.buffers.depth.setMask( true ); // make sure the depth buffer is writable so it can be properly cleared, see #18897
renderer.clear();
renderFunction.call(renderer, this.scene, this.camera);
this.data.outputElement.getObject3D('mesh').material.map = renderTarget.texture;
// restore original renderer settings.
renderer.setRenderTarget(currentRenderTarget);
renderer.xr.enabled = currentXrEnabled;
renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
this.activeRenderTarget = 1 - this.activeRenderTarget;
}
}
});

D3.js + CSS button functionality

I have been toiling away at this one particular graph that is a modified bullet chart from one of Mike Bostock's templates. I wanted to enhance it with some button events, specifically: when you click a button it will call a transition animation to change the graph markers to new values. Both new and old values are provided in the code in raw data form. For clarity and context I have included the code in it's entirety. Search for my comment "//confusion below" to get to the part where I defined my button functions.
<!DOCTYPE html>
<html>
<head>
<title>Bullet Chart</title>
<meta charset="utf-8">
</head>
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: auto;
padding-top: 40px;
position: relative;
/*width: 100%;*/
}
table{
width:60%;
margin-left:auto;
margin-right:auto;
}
td{width:50%;}
.bulleT { font: 10px sans-serif; margin-left:auto;margin-right:auto;}
.bulleT .marker { stroke: #4D4D4D; stroke-width: 2px;}
.bulleT .marker.s0 { fill-opacity:0; stroke: #999999; stroke-width: 2px; }
.bulleT .marker.s1 { fill-opacity:0; stroke: #000; stroke-width: 2px; }
.bulleT .tick line { stroke: #666; stroke-width: .5px; }
.bulleT .range.s0 { fill: #005C7A; }
.bulleT .range.s1 { fill: #29A3CC; }
.bulleT .range.s2 { fill: #c6dbef; }
.bulleT .range.s3 { fill: #29A3CC; }
.bulleT .range.s4 { fill: #005C7A; }
.bulleT .measure.s0 { fill: #4D4D4D; }
.bulleT .measure.s1 { fill: #999999; }
.bulleT .measure.s2 { fill: #eeeeee; }
.bulleT .measure.s3 { fill: #999999; }
.bulleT .measure.s4 { fill: #4D4D4D; }
.bulleT .title { font-size: 12px; font-weight: bold; }
.bulleT .subtitle.s04 { fill: #000000; font-size: 16px; font-weight: bold;}
.bulleT .subtitle.s13 { fill: #999999; font-size: 12px; font-weight: bold;}
.bulleT .subtitle.s2 { fill: #999999; font-size: 10px;}
.option {
font-family: Play;
color: #ffffff;
font-size: 12px;
width: 6%;
background: #303030;
padding: 10px 20px 10px 20px;
}
.option:hover {
background: #4c4d4d;
}
div#option2{
position:relative;
top: 10px;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<div id="canvas-svg">
<div id="canvas-svg">
<table>
<tr>
<td>
<div id="BulleT_horizontal"></div>
</td>
</tr>
</table>
</div>
<div id="option1" class="option"> Outcome 1</div>
<div id="option2" class="option"> Outcome 2</div>
<p>The Bullet Chart is here.</p>
<script>
(function() {
// Simple modification based on mbostock's Bullet Charts.
d3.bulleT = function() {
var orient = "left",
reverse = false,
vertical = false,
terjedelem = bulleTTerjedelem,
ranges = bulleTRanges,
markers = bulleTMarkers,
measures = bulleTMeasures,
width = 380,
height = 30,
tickFormat = null;
// For each small multiple
function bulleT(g) {
g.each(function(d, i) {
var terjedelemz = terjedelem.call(this, d, i),
rangez = ranges.call(this, d, i).slice().sort(d3.descending),
markerz = markers.call(this, d, i),
measurez = measures.call(this, d, i).slice().sort(d3.descending),
g = d3.select(this);
var wrap = g.select("g.wrap");
if (wrap.empty()) wrap = g.append("g").attr("class", "wrap");
// Compute the x-scale.
var x0 = d3.scale.linear()
.domain([terjedelemz[0], terjedelemz[1]])
.range(reverse ? [width, 0] : [0, width]);
// Stash the new scale.
this.__chart__ = x0;
// Derive width-scales from the x-scales.
var w = bulleTWidth(x0,terjedelemz[0]);
// Update the range rects.
rangez.unshift(terjedelemz[1]);
var range = wrap.selectAll("rect.range")
.data(rangez);
range.enter().append("rect")
.filter( function(d, i){ if(i != 3){ return d} })
.attr("class", function(d, i) { return "range s" + i; })
.attr("width", w)
.attr("y", 0)
.attr("height",height)
.attr("x", reverse ? x0 : 0);
range.enter().append("line")
.filter( function(d, i){ if(i == 3){ return d} })
.attr("class", "marker")
.attr("x1", x0)
.attr("x2", x0)
.attr("y1", 0)
.attr("y2", height);
// Append the measure rects.
measurez.unshift(terjedelemz[1]);
var measure = wrap.selectAll("rect.measure")
.data(measurez);
measure.enter().append("rect")
.attr("class", function(d, i) { return "measure s" + i; })
.attr("width", w)
.attr("height", height / 2)
.attr("x", reverse ? x0 : 0)
.attr("y", height / 4);
// Append rect and line marker.
var marker = wrap.selectAll("rect.marker")
.data(markerz);
marker.enter().append("rect")
.filter( function(d, i){ if(i == 1){ return d} })
.attr("class", "marker s1")
.attr("width", 6)
.attr("y", -(height/10))
.attr("height",function(d) {return height+(height/5);})
.attr("x", x0)
.attr("transform", "translate(-3,0)");
//confusion below
var option1 = d3.select("#option1");
option1.on('click.outcome', outcome1);
//option1.on('click.val', val=[100,200,1000,20]);
var option2 = d3.select("#option2");
option2.on('click.outcome', outcome2);
function outcome1(val) {
d.markerz = [1000, 2000, 10, 20]
d3.selectAll('rect.marker')
.transition()
.duration(3000)
.attr("x", function(d) {return d})
}
function outcome2(val) {
d.markerz = [-1000, -2000, -10, -20]
d3.selectAll('rect.marker')
.transition()
.duration(3000)
.attr("x", function(d) {return d})
};
marker.enter().append("line")
.filter( function(d, i){ if(i == 0){ return d} })
.attr("class", "marker s0")
.attr("x1", x0)
.attr("x2", x0)
.attr("y1", height / 4)
.attr("y2", height-(height / 4) );
// Compute the tick format.
var format = tickFormat || x0.tickFormat(8);
// Update the tick groups.
var tick = g.selectAll("tick")
.data(x0.ticks(8), function(d) {
return this.textContent || format(d);
});
// Initialize the ticks with the old scale, x0.
var tickEnter = tick.enter().append("g")
.attr("class", "tick")
.attr("transform", bulleTTranslate(x0))
.style("opacity", 1);
tickEnter.append("line")
.attr("y1", height)
.attr("y2", height * 7 / 6);
tickEnter.append("text")
.attr("text-anchor", "middle")
.attr("transform", function(d){
if (vertical) {
return "rotate(90)";
}
})
.attr("dy", function(d){
if(vertical){return width/60; }else{ return height+15 }
})
.attr("dx", function(d){
if(vertical){return height+15 ;}
})
.text(format);
});
}
// left, right, top, bottom
bulleT.orient = function(x) {
if (!arguments.length) return orient;
orient = x;
reverse = orient == "right" || orient == "bottom";
return bulleT;
};
// terjedelem
bulleT.terjedelem = function(x) {
if (!arguments.length) return terjedelem;
terjedelem = x;
return bulleT;
};
// ranges (bad, satisfactory, good)
bulleT.ranges = function(x) {
if (!arguments.length) return ranges;
ranges = x;
return bulleT;
};
//*
// markers (previous, goal)
bulleT.markers = function(x) {
if (!arguments.length) return markers;
markers = x;
return bulleT;
};
// measures (actual, forecast)
bulleT.measures = function(x) {
if (!arguments.length) return measures;
measures = x;
return bulleT;
};
//*/
bulleT.vertical = function(x) {
if (!arguments.length) return vertical;
vertical = x;
return bulleT;
};
bulleT.width = function(x) {
if (!arguments.length) return width;
width = x;
return bulleT;
};
bulleT.height = function(x) {
if (!arguments.length) return height;
height = x;
return bulleT;
};
bulleT.tickFormat = function(x) {
if (!arguments.length) return tickFormat;
tickFormat = x;
return bulleT;
};
return bulleT;
};
function bulleTTerjedelem(d) {
return d.terjedelem;
}
function bulleTRanges(d) {
return d.ranges;
}
function bulleTMarkers(d) {
return d.markers;
}
function bulleTMeasures(d) {
return d.measures;
}
function bulleTTranslate(x) {
return function(d) {
return "translate(" + x(d) + ",0)";
};
}
function bulleTWidth(x,y) {
var x0 = x(0);
return function(d) {
return Math.abs(x(d-y) - x0);
};
}
})();
var Tscore_Man_Height = -1019;
var Tscore_Woman_Height = -261;
var Tscore_Man_Weight = -4.64;
var Tscore_Woman_Weight = -1.6;
var Tscore2 = 1300
// terjedelem is the hungarian translation of the statistical term of range
var data = [
{"title":"Exper","dimension":"(diff)","subtitle":Tscore_Man_Height,"terjedelem":[-3000,3000],"ranges":[ -2000, -1000, 0, 1000, 2000],"measures":[-1366,-676,605,1108.81],"markers":[-23,Tscore_Man_Height]},
{"title":"Gold","dimension":"(diff)","subtitle":Tscore_Woman_Height,"terjedelem":[-8000,8000],"ranges":[-5250,-2750,0,2750,5250],"measures":[-901,110,845.5,2107.5],"markers":[412,Tscore_Woman_Height]},
{"title":"Tech","dimension":"(diff)","subtitle":Tscore_Man_Weight,"terjedelem":[-20,20],"ranges":[-13.33,-6.66,0,6.66,13.33],"measures":[-8.7,-.9,4.9,10.7],"markers":[1,Tscore_Man_Weight]},
{"title":"Units","dimension":"(diff)","subtitle":Tscore_Woman_Weight,"terjedelem":[-80,80],"ranges":[-53.33,-26.66,0,26.66,53.33],"measures":[-18,-7.8,3.8,23],"markers":[-3,Tscore_Woman_Weight]}
]
var Width = 400, Height = 50;
var margin = {top: 5, right: 20, bottom: 20, left: 60},
width = Width - margin.left - margin.right,
height = Height - margin.top - margin.bottom;
var chart = d3.bulleT()
.width(width)
.height(height);
function bulleT(whichData,whereToPut,direction) {
var a=Width, b=Height;
if( direction == "vertical"){
Height=a;Width=b+30;
vertical = true;
}else{
Height=a-20;Width=b;
vertical = false;
}
var svg = d3.select(whereToPut).selectAll("svg")
.data(whichData)
.enter().append("svg")
.attr("class", "bulleT")
.attr("width", Width)
.attr("height", Height)
.append("g")
.attr("transform", function(){
if( direction == "vertical"){
return "rotate(-90)translate("+ -(Height-margin.left) +",10)";
}else{
return "translate("+ margin.left +","+ margin.top +")";
}
})
.call(chart.vertical(vertical));
var title = svg.append("g")
.style("text-anchor", function(){
if( direction == "vertical"){
return "middle";
}else{
return "end";
}
})
.attr("transform", function(){
if( direction == "vertical"){
return "rotate(90)translate("+ Width/4 +",20)";
}else{
return "translate(-16," + height / 3 + ")";
}
});
title.append("text")
.attr("class", "title")
.text(function(d) { return d.title; });
title.append("text")
.attr("dy", "1.2em")
.text(function(d) { return d.dimension; })
title.append("text")
.attr("class",function(d) {
switch (true)
{
case ( (d.markers[1] < 30) || (70 < d.markers[1]) ):
return "subtitle s04";
break;
break;
case ( (30 <= d.markers[1]) && (d.markers[1] < 40) ):
return "subtitle s13";
break;
case ( (40 <= d.markers[1]) && (d.markers[1] <= 60) ):
return "subtitle s2";
break;
case ( (60 < d.markers[1]) && (d.markers[1] <= 70) ):
return "subtitle s13";
break;
}
}
)
.attr("dy", function(){
return "2.4em";
})
.text(function(d) { return d.subtitle; });
};
bulleT(data,"#BulleT_vertical","vertical"); // "horizontal" or "vertical"
bulleT(data,"#BulleT_horizontal","horizontal");
</script>
</body>
</html>
When I click the buttons, they both do the same thing, even though I changed markerz to different values. Moreover, they don't even do the right thing, the values seem to be wrong as some of the markers go flying off the graph boundary. So whatever I'm doing doesn't seem to change the way the data is parsed. Also, I'm not entirely sure that this raw value data would be scaled to go with the x scale of the graph, or if I need to scale it again as it goes through my button functions. The scaling seems to have something to do with x0 in the code above. I couldn't get it to work with my button for some reason.
There is a similar updated block here that has functional button functionality:
http://bl.ocks.org/CodeXmonk/6187523
My graph is different in that I don't change the measures or anything other than where the markers are positioned. And I'm not randomizing the data, I have pre-coded raw data that I want to switch back and forth from using my css buttons.
So what I'm looking for is how I can code the buttons to work with pre-existing scales and load raw data and put it into the markerz, or some other variable for the .transition() animation. That way the buttons will have the desired effect of moving the markers across the graph.
Sorry I couldn't make this shorter. I hope this will be worth everyone's while though because d3.js + css interface is a really powerful combination. I hope we can all learn from this example.
Thank you for reading
After further toiling, I have good news. I got it working by pure trial and error. I will post a partial answer here. I managed to get a functional chart in terms of using .transition() with precoded raw data in pixel form. Note: the only way I got this to work was by putting in the pixel value of x for the transition. Ideally, I'd like to just put in the raw data (in data form) and have the existing scale handle it to automatically compute the pixel values. I'm not going to be too picky for now. Here is what I have going so far. I took the trouble to post my own gist, because I want it to be easier to view than the lengthy code above.
https://bl.ocks.org/diggetybo/83188e161c39f2d0f7025087598ec075
I still need an answer on the following:
how to leverage existing scales alongside my raw data that is coupled with my css button functions
How do I update the subtitle text? It doesn't know I updated the values so they always read "1153, 1506, 8.63, 5.86". Ideally when I click Outcome 1, I want them to reflect the new marker positions: "-1019,-261,-4.64,-1.6". Then still be able to revert back to "1153, 1506, 8.63, 5.86" upon clicking the Outcome 2 button.
With regards to 2., I tried copying and pasting the title.append snippet for the marker values and using another mouse event for the cue, but each way I did it returned an error. Any experienced d3 people know how to do that?
Lastly, I'm not sure why, but every time I click a button, the dev tool error count increases by 1. The error is, "something something is not a function". I'm guessing that's not a good sign. However, the graph does work for the most part. I don't know how much I should be freaking out about these errors.
Thanks again
I think you have to use document.ready function for each variable and then give the .on click function.

JCrop selection on image problem in IE

I am using JCrop to crop an image after upload with Ajax upload
control in my web application. It works great on Chrome, Firefox but
not in IE. I use JCrop v0.9.12 (build: 20130202) and IE
v10.0.9.9200.16635. Problem is JCrop selection doesn't work in IE.
Thanks!
Here is my scripts.
<script type="text/javascript">
jQuery(document).ready
(function ($) {
// To hold the API and image size.
var jcrop_api, boundx, boundy;
$('#<%=imgCrop.ClientID%>').Jcrop (
{ // img_crop is the ID of image control
onChange: updatePreview, // will display the selected img on change.
onSelect: updatePreview, // will display the selected img Img_preview
onSelect: storeCoords, // will tell the coordinates
aspectRatio: 11 / 15
}, function ()
{
jcrop_api = this;
var bounds = this.getBounds();
boundx = bounds[0];
boundy = bounds[1];
}
);
function updatePreview(c) {
if (parseInt(c.w) > 0) {
var rx = 100 / c.w;
var ry = 100 / c.h;
$('#<%=Img_preview.ClientID%>').css({ //Img_preview is the ID of image control
width: Math.round(rx * boundx) + 'px',
height: Math.round(ry * boundy) + 'px',
marginLeft: '-' + Math.round(rx * c.x) + 'px',
marginTop: '-' + Math.round(ry * c.y) + 'px'
});
}
};
});
// will store the selected part the images coordinates
function storeCoords(c) {
jQuery('#<%=W.ClientID%>').val(c.w);
jQuery('#<%=H.ClientID%>').val(c.h);
jQuery('#<%=X.ClientID%>').val(c.x);
jQuery('#<%=Y.ClientID%>').val(c.y);
};
</script>
A common fix for IE is to do
$.Jcrop('#<%=imgCrop.ClientID%>', {
// your inits
});

Resources