Crossfilter for downsampling data in dc.js plot - dc.js

I'm trying to downsample an array that has ~10,000,000 points based on the current zoom state.
The downsampling mechanism is through downsample module, using LTTB function; below is a simple example of how it's done crossfilter
const ARRAY_LENGTH = 1e7;
const MAX_VAL = 40;
const randomArray = [];
for (let i = 0; i < ARRAY_LENGTH; i += 1) {
randomArray.push({ x: i, y: Math.floor(Math.random() * MAX_VAL) });
}
export { randomArray };
const chartWidth = 10000;
export const downsampledData = LTTB(randomArray, chartWidth);
export const downsampledDataArray = downsampledData as Array<{
x: number;
y: number;
}>;
Now I'm trying to figure out how to implement LTTB using crossfilter, so I can use zoom callback to only render the 10,000 points within the zoom window.
The simplified dc.js code is as follows:
const xy = crossfilter(downsampledDataArray);
const dimension = xy.dimension((d) => [d.x, d.y]);
const group = dimension.group();
const chart = dc
// eslint-disable-next-line #typescript-eslint/ban-ts-comment
// #ts-ignore
.lineChart(divRef) // #type error
.margins({ top: 10, right: 50, bottom: 50, left: 60 })
.x(d3.scaleLinear().domain([0, 1e7]))
.yAxisLabel('Photons/s')
.xAxisLabel('Time')
.keyAccessor((d) => {
return d.key[0];
})
.valueAccessor((d) => {
return d.key[1];
})
.clipPadding(10)
.renderArea(false)
.dimension(dimension)
.mouseZoomable(true)
// .excludedColor('#ddd')
.group(group);
Here's a stackblitz with chart rendering using the above pre-downsampled data

Related

Place several symbols on top of each candle on the canvas chart

Created a simplified example of what I need. There are 50 items in the series. Each item has a vertical list of five + symbols.
const data = [...Array(50).keys()];
const container = document.querySelector('d3fc-canvas');
const xScale = d3.scaleLinear().domain([0, data.length - 1]);
const yScale = d3.scaleLinear().domain(fc.extentLinear()(data));
const series = fc
.seriesCanvasPoint()
.xScale(xScale)
.yScale(yScale)
.crossValue((_, i) => i)
.mainValue(d => d)
.size((_, i) => 0)
.decorate((context, datum) => {
// Draw 5 symbols above current value
for (let i = 0; i < 5; i++) {
const y = yScale.invert(datum + i)
context.textAlign = 'center';
context.fillStyle = '#000';
context.font = '25px Arial';
context.fillText('+', 0, 0);
context.translate(0, y);
}
});
d3.select(container)
.on('draw', () => {
series(data);
})
.on('measure', () => {
const {
width,
height
} = event.detail;
xScale.range([0, width]);
yScale.range([height, 0]);
const ctx = container.querySelector('canvas').getContext('2d');
series.context(ctx);
});
container.requestRedraw();
body {
color: #1b1e23;
font-size: small;
font-family: sans-serif;
height: calc(100vh - 2em);
margin: 1em;
display: flex;
}
body>* {
flex: auto;
}
.domain,
.gridline-y,
.gridline-x,
.annotation-line>line {
stroke: currentColor;
stroke-opacity: 0.1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/d3fc/build/d3fc.js"></script>
<d3fc-canvas use-device-pixel-ratio></d3fc-canvas>
Now, I'm trying to make it work with an actual financial chart and place these five + symbols vertically within each candle. It doesn't work because I don't know how to correctly convert Y value of each + to the coordinate system of the canvas.
const stream = fc.randomFinancial().stream();
const data = stream.take(20);
const scaleX = d3.scalePoint();
const scaleY = d3.scaleLinear();
const yExtent = fc.extentLinear().accessors([d => d.high, d => d.low]);
const xExtent = fc.extentLinear().accessors([d => d.date.getTime()]);
const gridlines = fc.annotationCanvasGridline();
const candlestick = fc.seriesCanvasCandlestick()
.crossValue((o, i) => o.date.getTime())
.lowValue((o, i) => o.low)
.highValue((o, i) => o.high)
.openValue((o, i) => o.open)
.closeValue((o, i) => o.close);
const points = fc
.seriesCanvasPoint()
.crossValue((o, i) => o.date.getTime())
.mainValue(d => d.close)
.size((_, i) => 0)
.decorate((context, datum) => {
const arrows = getMarks(datum.open, datum.close);
//context.translate(0, 0);
// Draw 5 symbols above current value
for (let i = 0; i < arrows.length; i++) {
const posCurrent = scaleY.invert(arrows[i])
//context.translate(0, 15);
context.translate(0, posCurrent); // move to current arrow's position
context.textAlign = 'center';
context.fillStyle = '#000';
context.font = '25px Arial';
context.fillText('+', 0, 0);
context.translate(0, -posCurrent); // reset and go back to start point
}
});
const multi = fc
.seriesCanvasMulti()
.series([gridlines, candlestick, points]);
const chart = fc
.chartCartesian(scaleX, scaleY)
.canvasPlotArea(multi);
function getMarks(open, close) {
let price = close;
const arrows = [];
const gap = Math.abs(close - open) / 5;
for (let i = 0; i < 5; i++) {
arrows.push(close > open ? price -= gap : price += gap);
}
return arrows;
}
function renderChart() {
data.push(stream.next());
data.shift();
scaleX.domain(data.map(o => o.date.getTime()));
scaleY.domain(yExtent(data));
chart
.xDomain(scaleX.domain())
.yDomain(scaleY.domain());
d3.select('#chart')
.datum(data)
.call(chart);
}
renderChart();
setInterval(renderChart, 1000);
#chart {
height: 500px;
}
<script src="https://unpkg.com/d3"></script>
<script src="https://unpkg.com/d3fc"></script>
<div id="chart"></div>
Is there a way to properly show these marks on top of the candle?
Cross-reference to Github for more details and examples.
https://github.com/d3fc/d3fc/issues/1635
Firstly, scale.invert is meant for when you have the pixel coordinate, but not the corresponding datum value. This is relevant when you are doing clicks on a screen, for example. In this case, it's not the right tool for the job. You already have access to datum, so you just need to call scaleY(datum.close).
However, d3fc uses canvas.translate() to change the zero point to .mainvalue(). Therefore, you cannot simply use scaleY(datum.close) to get the y-coordinate of the close value. Instead of (x, y), each point you want to draw becomes (0, y(arrow) - y(d.close)). So you need to change the logic to reflect this.
Needing to clean up after yourself with context.translate() is a recipe for bugs, It's almost always better to avoid such things.
As a final note, I had some confusion about your getMarks function. If you want to draw 5 arrows evenly distributed over your bar, you need to have a gap of 1/4th of the bar height. Think of it like trees on a street. If the trees at 10 feet apart and the street is 100 feet long, you'll have 11 trees, not 10. Because of this, I drew 6 plus signs, not 5, because otherwise it looked kind of off balance.
const stream = fc.randomFinancial().stream();
const data = stream.take(20);
const scaleX = d3.scalePoint();
const scaleY = d3.scaleLinear();
const yExtent = fc.extentLinear().accessors([d => d.high, d => d.low]);
const xExtent = fc.extentLinear().accessors([d => d.date.getTime()]);
const gridlines = fc.annotationCanvasGridline();
const candlestick = fc.seriesCanvasCandlestick()
.crossValue((o, i) => o.date.getTime())
.lowValue((o, i) => o.low)
.highValue((o, i) => o.high)
.openValue((o, i) => o.open)
.closeValue((o, i) => o.close);
const points = fc
.seriesCanvasPoint()
.crossValue((o, i) => o.date.getTime())
.mainValue(d => d.close)
.size((_, i) => 0)
.decorate((context, datum) => {
const arrows = getMarks(datum.open, datum.close);
const top = scaleY(datum.close);
// We need a little bit of offset, because the "+" is 20 pixels high
// and we want it in the middle of the point, not above it.
const offset = 10;
// Draw 5 symbols above current value
for (let i = 0; i < arrows.length; i++) {
context.textAlign = 'center';
context.fillStyle = '#000';
context.font = '25px Arial';
context.fillText('+', 0, scaleY(arrows[i]) - top + offset);
}
});
const multi = fc
.seriesCanvasMulti()
.series([gridlines, candlestick, points]);
const chart = fc
.chartCartesian(scaleX, scaleY)
.canvasPlotArea(multi);
function getMarks(open, close) {
let price = Math.min(open, close);
const arrows = [];
const gap = Math.abs(close - open) / 5;
for (let i = 0; i < 6; i++) {
arrows.push(price + i * gap);
}
return arrows;
}
function renderChart() {
data.push(stream.next());
data.shift();
scaleX.domain(data.map(o => o.date.getTime()));
scaleY.domain(yExtent(data));
chart
.xDomain(scaleX.domain())
.yDomain(scaleY.domain());
d3.select('#chart')
.datum(data)
.call(chart);
}
renderChart();
setInterval(renderChart, 1000);
#chart {
height: 500px;
}
<script src="https://unpkg.com/d3"></script>
<script src="https://unpkg.com/d3fc"></script>
<div id="chart"></div>

How does raycasting in three.js work with an offscreen canvas?

I can't seem to get raycasting to work in an offscreen canvas.
A click event sends data to the worker like this:
var r = document.getElementById('webGL').getBoundingClientRect()
offscreen.postMessage({
action: 'set_scene',
mesh: mesh.toJSON(),
camera: camera.toJSON(),
canvasSize: {
width: document.getElementById('webGL').clientWidth,
height: document.getElementById('webGL').clientHeight
},
coordinates: { x: e.originalEvent.clientX - r.x, y: e.originalEvent.clientY - r.y },
time: (new Date())
});

while the worker looks like this:
self.importScripts( './three.min.js' );
var loader = new THREE.ObjectLoader();
var scene = new THREE.Scene();
self.onmessage = function(e) {
// var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
var canvas = new OffscreenCanvas(e.data.canvasSize.width, e.data.canvasSize.height);
var renderer = new THREE.WebGLRenderer( { antialias: true, canvas: canvas, preserveDrawingBuffer: true } );
Promise.all([
(new Promise(function(resolve, reject) {
loader.parse(
e.data.mesh,
function ( obj ) {
resolve( obj );
})
})),
(new Promise(function(resolve, reject) {
loader.parse(
e.data.camera,
function ( obj ) {
resolve( obj );
})
}))
]).then(obj => {
var mesh, camera
[mesh, camera] = obj;
scene.add( mesh );
renderer.render( scene, camera );
var raycaster = new THREE.Raycaster();
p = { x: e.data.coordinates.x, y: e.data.coordinates.y };
var m = {};
m.x = (p.x) / e.data.canvasSize.width * 2 - 1;
m.y = 1 - (p.y) / e.data.canvasSize.height * 2;
raycaster.setFromCamera( m, camera );
var intersects = raycaster.intersectObjects( [ mesh ], true );
return intersects;
}).then(r => {
self.postMessage(r);
}).catch(e => {
console.log(e);
})
}
Same code onscreen works ok, and the values resulting from the transforms check out ok.
Is it possible to do such a thing at all, or what am I getting wrong?
I don't see the issue in your code but there is absolutely nothing special about picking offscreen. Here's a working example to prove it. It doesn't have any three or camera or mesh in the main page, only in the worker.
All the main page does is start the worker, transfer control of the canvas to the worker, then send resize and mouse events to the worker. That's it. Otherwise the code in the worker is 99% the same as the code would be in the main page. The only major difference is resizeCanvasToDisplaySize gets the display size from state.width and state.height. The non-offscreen version code comes from here
You need to post more code.
function main() {
const canvas = document.querySelector("#c");
if (!canvas.transferControlToOffscreen) {
alert('no offscreen canvas support');
return;
}
const offscreen = canvas.transferControlToOffscreen();
const workerScript = document.querySelector('#foo').text;
const blob = new Blob([workerScript], {type: 'application/javascript'});
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);
function sendSize() {
worker.postMessage({
type: 'size',
width: canvas.clientWidth,
height: canvas.clientHeight,
});
}
sendSize();
window.addEventListener('resize', sendSize);
function getNormaizedMousePosition(element, e) {
const rect = element.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
return {
x: x / canvas.clientWidth * 2 - 1,
y: y / canvas.clientHeight * -2 + 1,
}
}
canvas.addEventListener('mousemove', (e) => {
const pos = getNormaizedMousePosition(canvas, e);
worker.postMessage({
type: 'mousemove',
x: pos.x,
y: pos.y,
});
});
}
main();
body {
margin: 0;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
<canvas id="c"></canvas>
<script type="foo" id="foo">
'use strict';
importScripts('https://threejsfundamentals.org/threejs/resources/threejs/r103/three.min.js');
/* global THREE */
const state = {
width: 300,
height: 150,
mouse: {
x: -2,
y: -2,
},
};
function init(data) {
const {canvas} = data;
const renderer = new THREE.WebGLRenderer({
canvas
});
state.width = canvas.width;
state.height = canvas.height;
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 100;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 4;
const scene = new THREE.Scene();
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
function makeInstance(geometry, color, x) {
const material = new THREE.MeshPhongMaterial({
color
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
cube.position.x = x;
return cube;
}
const cubes = [
makeInstance(geometry, 0x44aa88, 0),
makeInstance(geometry, 0x8844aa, -2),
makeInstance(geometry, 0xaa8844, 2),
];
class PickHelper {
constructor() {
this.raycaster = new THREE.Raycaster();
this.pickedObject = null;
this.pickedObjectSavedColor = 0;
}
pick(normalizedPosition, scene, camera, time) {
// restore the color if there is a picked object
if (this.pickedObject) {
this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
this.pickedObject = undefined;
}
// cast a ray through the frustum
this.raycaster.setFromCamera(normalizedPosition, camera);
// get the list of objects the ray intersected
const intersectedObjects = this.raycaster.intersectObjects(scene.children);
if (intersectedObjects.length) {
// pick the first object. It's the closest one
this.pickedObject = intersectedObjects[0].object;
// save its color
this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
// set its emissive color to flashing red/yellow
this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
}
}
}
const pickHelper = new PickHelper();
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = state.width;
const height = state.height;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.width / canvas.height;
camera.updateProjectionMatrix();
}
cubes.forEach((cube, ndx) => {
const speed = 1 + ndx * .1;
const rot = time * speed;
cube.rotation.x = rot;
cube.rotation.y = rot;
});
pickHelper.pick(state.mouse, scene, camera, time);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
function size(data) {
state.width = data.width;
state.height = data.height;
}
function mousemove(data) {
state.mouse.x = data.x;
state.mouse.y = data.y;
}
const handlers = {
init,
size,
mousemove,
};
self.onmessage = function(e) {
const fn = handlers[e.data.type];
if (!fn) {
throw new Error('no handler for type: ' + e.data.type);
}
fn(e.data);
};
</script>
For your particular case though you shouldn't need a camera. Send the ray. You don't need the canvas size either. Picking in three.js is CPU based
function main() {
const canvas = document.querySelector("#c");
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 60;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 200;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 4;
const scene = new THREE.Scene();
scene.background = new THREE.Color('white');
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
function makeInstance(geometry, color, x, name) {
const material = new THREE.MeshPhongMaterial({
color
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
cube.name = name;
cube.position.x = x;
cube.rotation.x = x;
cube.rotation.z = x + .7;
return cube;
}
makeInstance(geometry, 0x44aa88, 0, 'cyan cube');
makeInstance(geometry, 0x8844aa, -2, 'purple cube');
makeInstance(geometry, 0xaa8844, 2, 'brown cube');
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render() {
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.width / canvas.height;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
}
render();
window.addEventListener('resize', render);
const workerScript = document.querySelector('#foo').text;
const blob = new Blob([workerScript], {type: 'application/javascript'});
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
const msgElem = document.querySelector('#msg');
worker.onmessage = (e) => {
msgElem.textContent = e.data;
};
worker.postMessage({type: 'init', scene: scene.toJSON()});
function getNormaizedMousePosition(element, e) {
const rect = element.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
return {
x: x / canvas.clientWidth * 2 - 1,
y: y / canvas.clientHeight * -2 + 1,
}
}
const raycaster = new THREE.Raycaster();
canvas.addEventListener('mousemove', (e) => {
const pos = getNormaizedMousePosition(canvas, e);
raycaster.setFromCamera(pos, camera);
worker.postMessage({
type: 'intersect',
origin: raycaster.ray.origin.toArray(),
direction: raycaster.ray.direction.toArray(),
});
});
}
main();
body {
margin: 0;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
#msg {
position: absolute;
left: 1em;
top: 1em;
}
<canvas id="c"></canvas>
<div id="msg"></div>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r103/three.min.js"></script>
<script type="foo" id="foo">
'use strict';
importScripts('https://threejsfundamentals.org/threejs/resources/threejs/r103/three.min.js');
/* global THREE */
function loadObject(json) {
return new Promise((resolve) => {
const loader = new THREE.ObjectLoader();
loader.parse(json, resolve);
});
}
const renderer = new THREE.WebGLRenderer({
canvas: new OffscreenCanvas(1, 1),
});
// settings not important
const camera = new THREE.PerspectiveCamera(1, 1, 0.1, 100);
const raycaster = new THREE.Raycaster();
let scene;
let lastIntersectedObject;
async function init(data) {
scene = await loadObject(data.scene);
// we only need to render once to init the scene
renderer.render(scene, camera);
}
function intersect(data) {
raycaster.set(
new THREE.Vector3(...data.origin),
new THREE.Vector3(...data.direction));
const intersections = raycaster.intersectObjects(scene.children);
const intersectedObject = intersections.length
? intersections[0].object
: null;
if (intersectedObject !== lastIntersectedObject) {
lastIntersectedObject = intersectedObject;
log('intersection:', lastIntersectedObject ? lastIntersectedObject.name : 'none');
}
}
const handlers = {
init,
intersect,
};
self.onmessage = function(e) {
const fn = handlers[e.data.type];
if (!fn) {
throw new Error('no handler for type: ' + e.data.type);
}
fn(e.data);
};
function log(...args) {
postMessage([...args].join(' '));
}
</script>

Animated wireframe lines

I'm just curious if anyone has an idea how to achieve such wireframe "fade in" drawing line by line effect?
Maybe not exact but similar to such svg animation to make it more clear and easier to visualise https://maxwellito.github.io/vivus/
Webgl example here https://www.orano.group/experience/innovation/en/slider if you switch between the slides.
You need to give every element you want to draw a number in the order you want them drawn. For example if you want to draw a wireframe pass in a number for each vertex in the order you want them drawn, pass that number from the vertex shader to the fragment shader, then pass in a time. If the number is greater than the number discard (or in some other way don't draw)
Example:
'use strict';
/* global THREE */
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas: canvas});
const fov = 40;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 1000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 25;
const scene = new THREE.Scene();
scene.background = new THREE.Color('white');
const objects = [];
{
const width = 8;
const height = 8;
const depth = 8;
// using edges just to get rid of the lines triangles
const geometry = new THREE.EdgesGeometry(new THREE.BoxBufferGeometry(width, height, depth));
const numVertices = geometry.getAttribute('position').count;
const counts = new Float32Array(numVertices);
// every 2 points is one line segment so we want the numbers to go
// 0, 1, 1, 2, 2, 3, 3, 4, 4, 5 etc
const numSegments = numVertices / 2;
for (let seg = 0; seg < numSegments; ++seg) {
const off = seg * 2;
counts[off + 0] = seg;
counts[off + 1] = seg + 1;
}
const itemSize = 1;
const normalized = false;
const colorAttrib = new THREE.BufferAttribute(counts, itemSize, normalized); geometry.addAttribute('count', colorAttrib);
const timeLineShader = {
uniforms: {
color: { value: new THREE.Color('red'), },
time: { value: 0 },
},
vertexShader: `
attribute float count;
varying float vCount;
void main() {
vCount = count;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1);
}
`,
fragmentShader: `
#include <common>
varying float vCount;
uniform vec3 color;
uniform float time;
void main() {
if (vCount > time) {
discard;
}
gl_FragColor = vec4(color, 1);
}
`,
};
const material = new THREE.ShaderMaterial(timeLineShader);
const mesh = new THREE.LineSegments(geometry, material);
scene.add(mesh);
objects.push(mesh);
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
objects.forEach((obj, ndx) => {
const speed = .1 + ndx * .05;
const rot = time * speed;
obj.rotation.x = rot;
obj.rotation.y = rot;
obj.material.uniforms.time.value = (time * 4) % 15;
});
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
body { margin: 0; }
#c { width: 100vw; height: 100vh; display: block; }
<canvas id="c"></canvas>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r98/three.min.js"></script>
If you want multiple objects to draw consecutively just adjust the time for each one
'use strict';
/* global THREE */
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas: canvas});
const fov = 40;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 1000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 15;
const scene = new THREE.Scene();
scene.background = new THREE.Color('white');
const objects = [];
{
const width = 2;
const height = 2;
const depth = 2;
// using edges just to get rid of the lines triangles
const geometry = new THREE.EdgesGeometry(new THREE.BoxBufferGeometry(width, height, depth));
const numVertices = geometry.getAttribute('position').count;
const counts = new Float32Array(numVertices);
// every 2 points is one line segment so we want the numbers to go
// 0, 1, 1, 2, 2, 3, 3, 4, 4, 5 etc
const numSegments = numVertices / 2;
for (let seg = 0; seg < numSegments; ++seg) {
const off = seg * 2;
counts[off + 0] = seg;
counts[off + 1] = seg + 1;
}
const itemSize = 1;
const normalized = false;
const colorAttrib = new THREE.BufferAttribute(counts, itemSize, normalized); geometry.addAttribute('count', colorAttrib);
const timeLineShader = {
vertexShader: `
attribute float count;
varying float vCount;
void main() {
vCount = count;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1);
}
`,
fragmentShader: `
#include <common>
varying float vCount;
uniform vec3 color;
uniform float time;
void main() {
if (vCount > time) {
discard;
}
gl_FragColor = vec4(color, 1);
}
`,
};
for (let x = -2; x <= 2; x += 1) {
timeLineShader.uniforms = {
color: { value: new THREE.Color('red'), },
time: { value: 0 },
};
const material = new THREE.ShaderMaterial(timeLineShader);
const mesh = new THREE.LineSegments(geometry, material);
scene.add(mesh);
mesh.position.x = x * 4;
objects.push(mesh);
}
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
objects.forEach((obj, ndx) => {
const rotSpeed = .1;
const rot = time * rotSpeed;
obj.rotation.x = rot;
obj.rotation.y = rot;
const segmentsPer = 12;
const speed = 8;
const totalTime = segmentsPer * objects.length + 5 * speed;
obj.material.uniforms.time.value = ((time * speed) % totalTime) - ndx * segmentsPer;
});
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
body { margin: 0; }
#c { width: 100vw; height: 100vh; display: block; }
<canvas id="c"></canvas>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r98/three.min.js"></script>
Note that using a count will make each segment take the same amount of time to appear. If you want them to take longer by distance than instead of adding 1 to each segment you'd need to add the distance to the next point
distanceSoFar = 0;
for each segment
data.push(distanceSoFar);
distanceSoFar += distance(segmentStartPosition, segmentEndPosition);
data.push(distanceSoFar);
}

Can I create point cloud from depth and rgb image?

I am new to 3js, I have 2 images ie RGB image and Depth image. Can I create a point cloud combining these two using 3js?
if yes then how?
To solve this problem I went the three.js examples and searched for "point". I checked each matching sample for one that had different colors for each particle. Then I clicked the "view source" button to checkout the code. I ended up starting with this example and looked at the source. It made it pretty clear how to make a set of points of different colors.
So after that I just needed to load the 2 images, RGB and Depth, make a grid of points, for each point set the Z position to the depth and the color to the color of the image.
I used my phone to take these RGB and Depth images using this app
To get the data I draw the image into a canvas and then call getImageData. That gives me the data in values from 0 to 255 for each channel, red, green, blue, alpha.
I then wrote a function to get a single pixel out and return the colors in the 0 to 1 range. Just to be safe it checks the boundaries.
// return the pixel at UV coordinates (0 to 1) in 0 to 1 values
function getPixel(imageData, u, v) {
const x = u * (imageData.width - 1) | 0;
const y = v * (imageData.height - 1) | 0;
if (x < 0 || x >= imageData.width || y < 0 || y >= imageData.height) {
return [0, 0, 0, 0];
} else {
const offset = (y * imageData.width + x) * 4;
return Array.from(imageData.data.slice(offset, offset + 4)).map(v => v / 255);
}
}
result
'use strict';
/* global THREE */
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = (e) => { resolve(img); };
img.onerror = reject;
img.src = url;
});
}
function getImageData(img) {
const ctx = document.createElement("canvas").getContext("2d");
ctx.canvas.width = img.width;
ctx.canvas.height = img.height;
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
}
// return the pixel at UV coordinates (0 to 1) in 0 to 1 values
function getPixel(imageData, u, v) {
const x = u * (imageData.width - 1) | 0;
const y = v * (imageData.height - 1) | 0;
if (x < 0 || x >= imageData.width || y < 0 || y >= imageData.height) {
return [0, 0, 0, 0];
} else {
const offset = (y * imageData.width + x) * 4;
return Array.from(imageData.data.slice(offset, offset + 4)).map(v => v / 255);
}
}
async function main() {
const images = await Promise.all([
loadImage("https://i.imgur.com/UKBsvV0.jpg"), // RGB
loadImage("https://i.imgur.com/arPMCZl.jpg"), // Depth
]);
const data = images.map(getImageData);
const canvas = document.querySelector('canvas');
const renderer = new THREE.WebGLRenderer({canvas: canvas});
const fov = 75;
const aspect = 2; // the canvas default
const near = 1;
const far = 4000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 2000;
const controls = new THREE.OrbitControls(camera, canvas);
controls.target.set(0, 0, 0);
controls.update();
const scene = new THREE.Scene();
const rgbData = data[0];
const depthData = data[1];
const skip = 20;
const across = Math.ceil(rgbData.width / skip);
const down = Math.ceil(rgbData.height / skip);
const positions = [];
const colors = [];
const color = new THREE.Color();
const spread = 1000;
const depthSpread = 1000;
const imageAspect = rgbData.width / rgbData.height;
for (let y = 0; y < down; ++y) {
const v = y / (down - 1);
for (let x = 0; x < across; ++x) {
const u = x / (across - 1);
const rgb = getPixel(rgbData, u, v);
const depth = 1 - getPixel(depthData, u, v)[0];
positions.push(
(u * 2 - 1) * spread * imageAspect,
(v * -2 + 1) * spread,
depth * depthSpread,
);
colors.push( ...rgb.slice(0,3) );
}
}
const geometry = new THREE.BufferGeometry();
geometry.addAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
geometry.addAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
geometry.computeBoundingSphere();
const material = new THREE.PointsMaterial( { size: 15, vertexColors: THREE.VertexColors } );
const points = new THREE.Points( geometry, material );
scene.add( points );
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
body {
margin: 0;
}
canvas {
width: 100vw;
height: 100vh;
display: block;
}
<canvas></canvas>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r94/three.min.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r94/js/controls/OrbitControls.js"></script>

Can't Detect Mouseover on This KineticJS Shape?

I have a KineticJS shape that draws a bezier curve that is wider on one end. It draws correctly, but I can't yet detect a 'mouseover' event on it. I have created a small JSFiddle demo of the anomaly, at:
http://jsfiddle.net/VikR0001/nZYxL/6/
How can I detect 'mouseover' events on this shape?
Thanks very much in advance to all for any info!
var mainLayer;
//bezier curve code:
//http://stackoverflow.com/questions/8325680/how-to-draw-a-bezier-curve-with-variable-thickness-on-an-html-canvas
//draw a bezier curve that gets larger as it flows
//adapted for use with KineticJS
function drawBezierCurve() {
var centerLeft = new Object();
centerLeft.x = 100;
centerLeft.y = 400;
var centerRight = new Object();
centerRight.x = 400;
centerRight.y = 100;
var thicknessLeft = 1;
var thicknessRight = 50;
var color = "#000";
var context = mainLayer.getContext();
var leftUpper = {
x: centerLeft.x,
y: centerLeft.y - thicknessLeft / 2
};
var leftLower = {
x: centerLeft.x,
y: leftUpper.y + thicknessLeft
};
var rightUpper = {
x: centerRight.x,
y: centerRight.y - thicknessRight / 2
};
var rightLower = {
x: centerRight.x,
y: rightUpper.y + thicknessRight
};
var center = (centerRight.x + centerLeft.x) / 2;
var cp1Upper = {
x: center,
y: leftUpper.y
};
var cp2Upper = {
x: center,
y: rightUpper.y
};
var cp1Lower = {
x: center,
y: rightLower.y
};
var cp2Lower = {
x: center,
y: leftLower.y
};
var bezierCurve = new Kinetic.Shape({
drawFunc: function (canvas) {
var context = mainLayer.getContext();
context.fillStyle = color;
context.beginPath();
context.moveTo(leftUpper.x, leftUpper.y);
context.bezierCurveTo(cp1Upper.x, cp1Upper.y, cp2Upper.x, cp2Upper.y, rightUpper.x, rightUpper.y);
context.lineTo(rightLower.x, rightLower.y);
context.bezierCurveTo(cp1Lower.x, cp1Lower.y, cp2Lower.x, cp2Lower.y, leftLower.x, leftLower.y);
context.lineTo(leftUpper.x, leftUpper.y);
context.fill();
canvas.stroke(this);
},
fill: color,
stroke: color,
strokeWidth: 1
});
bezierCurve.on('mouseover', function (evt) {
document.body.style.cursor = "pointer";
$("#debug").html("MOUSEOVER DETECTED."); //<==NEVER CALLED
});
bezierCurve.on('mouseout', function (evt) {
document.body.style.cursor = "default";
$("#debug").html("MOUSEOUT DETECTED."); //NEVER CALLED
});
bezierCurve.setAttrs({
'leftUpper': leftUpper,
'leftLower': leftLower,
'rightUpper': rightUpper,
'rightLower': rightLower,
'cp1Upper': cp1Upper,
'cp2Upper': cp2Upper,
'cp1Lower': cp1Lower,
'cp2Lower': cp2Lower
});
mainLayer.add(bezierCurve);
mainLayer.draw();
$("#debug").html("bezier curve has been drawn onscreen.");
}
$(document).ready(function () {
var stage = new Kinetic.Stage({
container: 'canvasContainer',
width: 500,
height: 500
});
mainLayer = new Kinetic.Layer('main');
stage.add(mainLayer);
mainLayer.draw();
drawBezierCurve();
});
Can you define it as an SVG element, and just give that an onmouseover?
Fixed it! Changes are shown at the jsFiddle link in the original post.
//FIXED!
//OLD VERSION: DOES NOT WORK
// var bezierCurve = new Kinetic.Shape({
// drawFunc: function (canvas) {
// var context = mainLayer.getContext();
// context.fillStyle = color;
// context.beginPath();
// context.moveTo(leftUpper.x, leftUpper.y);
// context.bezierCurveTo(cp1Upper.x, cp1Upper.y, cp2Upper.x, cp2Upper.y, rightUpper.x, rightUpper.y);
// context.lineTo(rightLower.x, rightLower.y);
// context.bezierCurveTo(cp1Lower.x, cp1Lower.y, cp2Lower.x, cp2Lower.y, leftLower.x, leftLower.y);
// context.lineTo(leftUpper.x, leftUpper.y);
// context.closePath();
// context.fill();
// canvas.stroke(this);
// },
// fill: color,
// stroke: color,
// strokeWidth: 1
// });
//NEW VERSION: WORKS!
var bezierCurve = new Kinetic.Shape({
drawFunc: function (canvas) {
var context = canvas.getContext('2d');
context.beginPath();
context.moveTo(leftUpper.x, leftUpper.y);
context.bezierCurveTo(cp1Upper.x,cp1Upper.y, cp2Upper.x,cp2Upper.y, rightUpper.x,rightUpper.y);
context.lineTo(rightLower.x, rightLower.y);
context.bezierCurveTo(cp1Lower.x,cp1Lower.y, cp2Lower.x,cp2Lower.y, leftLower.x,leftLower.y);
context.lineTo(leftUpper.x, leftUpper.y);
context.fill();
canvas.stroke(this);
},
fill: color,
stroke: color,
strokeWidth: 3
});

Resources