I am working on a canvas library and I have the following class:
export default class Layer {
constructor(ypcanvas) {
this.ypcanvas = ypcanvas;
this.container = ypcanvas.container;
this.can = document.createElement("canvas");
this.ctx = this.can.getContext("2d");
}}
(This is just a little excerpt of the Layer class)
You can import the class and then create a new Layer like this:
let layer = new Layer(ypcanvas);
How could i accomplish an event like the following:
layer.on('mouseout', function () { });
or
layer.mousedown(function () { })
Or somethign equivalent to that, so that the user of my library can just call that event without having to addEventListener the layer canvas.
Thx in advance.
You can do something like this:
class MyLibClass {
constructor(elementId) {
this.elementId = elementId
}
mouseDown(callback) {
document.getElementById(this.elementId).addEventListener("mousedown", callback)
}
}
new MyLibClass("lib").mouseDown(() => alert("hey"))
#lib {
width: 100px;
height: 100px;
background: blue;
}
<div id="lib"></div>
Assuming you're going to add the canvas somehow to the document tree – otherwise you should describe more in details your system –, you can just proxy the lister to the canvas:
export default class Layer {
constructor(ypcanvas) {
this.ypcanvas = ypcanvas;
this.container = ypcanvas.container;
this.can = document.createElement("canvas");
this.ctx = this.can.getContext("2d");
}
on(type, lister) {
this.can.addEventListener(type, lister);
}
off(type,lister) {
this.can.removeEventListener(type, lister);
}
}
If instead it's just a memory canvas, then you have to implement a coordinates system and z-index to emit the right event from the physical canvas based on the layer copied – and that is indeed more complicated.
However, it's still the same principle: if you don't want that the final user adds the events to the canvas, you have to do it yourself at a certain point.
Related
I was trying to use a canvas as texture in my aframe project. I found some instructions here. It mentioned:
The texture will automatically refresh itself as the canvas changes.
However, I gave it a try today and the canvas could only be changed / updated in init function. Afterwards the update to canvas cannot be reflected. Here is my implementation:
module.exports = {
'canvas_component': {
schema: {
canvasId: { type: 'string' }
},
init: function () {
this.canvas = document.getElementById(this.data.canvasId);
this.ctx = this.canvas.getContext('2d');
this.ctx.fillStyle = "#FF0000";
this.ctx.fillRect(20, 20, 150, 100);
setTimeout(() => {
this.ctx.fillStyle = "#FFFF00";
this.ctx.fillRect(20, 20, 150, 100);
}, 2000);
}
}
The color change of of the texture was never changed. Is there anything I missed? Thank you so much for any advice.
I could never get it to work with those instructions (never checked out if bug or improper use though), but you can achieve the same with Three.js:
// assuming this is inside an aframe component
init: function() {
// we'll update this manually
this.texture = null
let canvas = document.getElementById("source-canvas");
// wait until the element is ready
this.el.addEventListener('loaded', e => {
// create the texture
this.texture = new THREE.CanvasTexture(canvas);
// get the references neccesary to swap the texture
let mesh = this.el.getObject3D('mesh')
mesh.material.map = this.texture
// if there was a map before, you should dispose it
})
},
tick: function() {
// if the texture is created - update it
if (this.texture) this.texture.needsUpdate = true
}
Check it out in this glitch.
Instead using the tick function, you could update the texture whenever you get any callback from changing the canvas (mouse events, source change).
The docs are out of date, I've made a pull request to update them. Here is the code that shows how to do it now:
src: https://github.com/aframevr/aframe/issues/4878
which points to: https://github.com/aframevr/aframe/blob/b164623dfa0d2548158f4b7da06157497cd4ea29/examples/test/canvas-texture/components/canvas-updater.js
We can quickly turn that into a component like this, for example:
/* global AFRAME */
AFRAME.registerComponent('live-canvas', {
dependencies: ['geometry', 'material'],
schema: {
src: { type: "string", default: "#id"}
},
init() {
if (!document.querySelector(this.data.src)) {
console.error("no such canvas")
return
}
this.el.setAttribute('material',{src:this.data.src})
},
tick() {
var el = this.el;
var material;
material = el.getObject3D('mesh').material;
if (!material.map) {
console.error("no material map")
this.el.removeAttribute('live-canvas')
return;
}
material.map.needsUpdate = true;
}
});
(remember to declate your components before your scene...)
usage:
<body>
<canvas id="example-canvas"></canvas>
<a-scene>
<a-box live-canvas="src:#example-canvas;"></a-box>
</a-scene>
</body>
live glitch code demo here:
https://glitch.com/edit/#!/live-canvas-demo?path=index.html%3A58%3A43
You can of course be more efficient than a tick handler if you just intentionally run the equivalent code manually whenever you update the canvas yourself, if that makes more sense / isn't happening frame-by-frame.
I have a simple linear chart built with Chart.js library.
And i want to allow user to drag points on chart for dynamically change data of it. I tied chartjs-plugin-draggable but it works for me only with annotations. I need graph exactly like this:
https://www.rgraph.net/canvas/docs/adjusting-line.html
But use new graph library in project is not good solution :(
Also i tried to play with dot event's.
UPDATE:
With angular i created something like this.
Maybe if there is no way to add drag&drop to points, there will be a hack to put "sliders" with absolute position on graph on points positions. I didn't find any info too :(
In case anyone is looking for a solution that doesn't require the use of plugins, it's pretty straightforward to do it in vanilla chart.js.
Here's a simple working example - just click and drag a data point
// some data to be plotted
var x_data = [1500,1600,1700,1750,1800,1850,1900,1950,1999,2050];
var y_data_1 = [86,114,106,106,107,111,133,221,783,2478];
var y_data_2 = [2000,700,200,100,100,100,100,50,25,0];
// globals
var activePoint = null;
var canvas = null;
// draw a line chart on the canvas context
window.onload = function () {
// Draw a line chart with two data sets
var ctx = document.getElementById("canvas").getContext("2d");
canvas = document.getElementById("canvas");
window.myChart = Chart.Line(ctx, {
data: {
labels: x_data,
datasets: [
{
data: y_data_1,
label: "Data 1",
borderColor: "#3e95cd",
fill: false
},
{
data: y_data_2,
label: "Data 2",
borderColor: "#cd953e",
fill: false
}
]
},
options: {
animation: {
duration: 0
},
tooltips: {
mode: 'nearest'
}
}
});
// set pointer event handlers for canvas element
canvas.onpointerdown = down_handler;
canvas.onpointerup = up_handler;
canvas.onpointermove = null;
};
function down_handler(event) {
// check for data point near event location
const points = window.myChart.getElementAtEvent(event, {intersect: false});
if (points.length > 0) {
// grab nearest point, start dragging
activePoint = points[0];
canvas.onpointermove = move_handler;
};
};
function up_handler(event) {
// release grabbed point, stop dragging
activePoint = null;
canvas.onpointermove = null;
};
function move_handler(event)
{
// locate grabbed point in chart data
if (activePoint != null) {
var data = activePoint._chart.data;
var datasetIndex = activePoint._datasetIndex;
// read mouse position
const helpers = Chart.helpers;
var position = helpers.getRelativePosition(event, myChart);
// convert mouse position to chart y axis value
var chartArea = window.myChart.chartArea;
var yAxis = window.myChart.scales["y-axis-0"];
var yValue = map(position.y, chartArea.bottom, chartArea.top, yAxis.min, yAxis.max);
// update y value of active data point
data.datasets[datasetIndex].data[activePoint._index] = yValue;
window.myChart.update();
};
};
// map value to other coordinate system
function map(value, start1, stop1, start2, stop2) {
return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1))
};
body {
font-family: Helvetica Neue, Arial, sans-serif;
text-align: center;
}
.wrapper {
max-width: 800px;
margin: 50px auto;
}
h1 {
font-weight: 200;
font-size: 3em;
margin: 0 0 0.1em 0;
}
h2 {
font-weight: 200;
font-size: 0.9em;
margin: 0 0 50px;
color: #555;
}
a {
margin-top: 50px;
display: block;
color: #3e95cd;
}
<!DOCTYPE html>
<html>
<!-- HEAD element: load the stylesheet and the chart.js library -->
<head>
<title>Draggable Points</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js#2.9.3/dist/Chart.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<!-- BODY element: create a canvas and render a chart on it -->
<body>
<!-- canvas element in a container -->
<div class="wrapper">
<canvas id="canvas" width="1600" height="900"></canvas>
</div>
<!-- call external script to create and render a chart on the canvas -->
<script src="script.js"></script>
</body>
</html>
Update: My previous answer got deleted because it only featured a link to a plugin solving the issue, however here comes the explanation to what it does:
The general procedure on how to achieve the desired behaviour is to
Intercept a mousedown (and check if it's a dragging gesture) on a given chart
Check if the mousedown was over a data point using the getElementAtEvent function
On mousemove, translate the new Y-Pixel value into a data coordinate using the axis.getValueForPixel function
Synchronously update the chart data using chart.update(0)
as pointed out in this Chart.js issue.
In order to intercept the mousedown, mousemove and mouseup events (the dragging gesture), event listeners for said events need to be created. In order to simplify the creation of the listeners one may use the d3 library in this case as follows:
d3.select(chartInstance.chart.canvas).call(
d3.drag().container(chartInstance.chart.canvas)
.on('start', getElement)
.on('drag', updateData)
.on('end', callback)
);
On mousedown (the 'start' event here), a function (getElement) may be called thatfetches the closest chart element to the pointers location and gets the ID of the Y-Scale
function getElement () {
var e = d3.event.sourceEvent
element = chartInstance.getElementAtEvent(e)[0]
scale = element['_yScale'].id
}
On mousemove ('drag'), the chart data is supposed to be updated according to the current Y-Pixel value of the pointer. We can therefore create an updateData function that gets the position of the clicked data point in the charts data array and the according dataset like this
function updateData () {
var e = d3.event.sourceEvent
var datasetIndex = element['_datasetIndex']
var index = element['_index']
var value = chartInstance.scales[scale].getValueForPixel(e.clientY)
chartInstance.data.datasets[datasetIndex].data[index] = value
chartInstance.update(0)
}
And that's it! If you need to store the resulting value after dragging, you may also specify a callback function like this
function callback () {
var datasetIndex = element['_datasetIndex']
var index = element['_index']
var value = chartInstance.data.datasets[datasetIndex].data[index]
// e.g. store value in database
}
Here is a working fiddle of the above code. The functionality is also the core of the Chart.js Plugin dragData, which may be easier to implement in many cases.
Here is how I fixed using both touchscreen or mouse event x,y coordinates for the excellent d3 example above by wrapping event screen coordinates in a more "generic" x,y object.
(Probably d3 has something similar to handle both types of events but lot of reading to find out..)
//Get an class of {points: [{x, y},], type: event.type} clicked or touched
function getEventPoints(event)
{
var retval = {point: [], type: event.type};
//Get x,y of mouse point or touch event
if (event.type.startsWith("touch")) {
//Return x,y of one or more touches
//Note 'changedTouches' has missing iterators and can not be iterated with forEach
for (var i = 0; i < event.changedTouches.length; i++) {
var touch = event.changedTouches.item(i);
retval.point.push({ x: touch.clientX, y: touch.clientY })
}
}
else if (event.type.startsWith("mouse")) {
//Return x,y of mouse event
retval.point.push({ x: event.layerX, y: event.layerY })
}
return retval;
}
.. and here is how I would use it in the above d3 example to store the initial grab point Y. And works for both mouse and touch.
Check the Fiddle
Here how I solved the problem with using d3 and wanting to drag the document on mobile or touch screens. Somehow with the d3 event subscription all Chart area events where already blocked from bubbling up the DOM.
Was not able to figure out if d3 could be configured to pass canvas events on without touching them. So in a protest I just eliminated d3 as it was not much involved other than subscribing events.
Not being a Javascript master this is some fun code that subscribes the events the old way. To prevent chart touches from dragging the screen only when a chart point is grabed each of the handlers just have to return true and the event.preventDefault() is called to keep the event to your self.
//ChartJs event handler attaching events to chart canvas
const chartEventHandler = {
//Call init with a ChartJs Chart instance to apply mouse and touch events to its canvas.
init(chartInstance) {
//Event handler for event types subscribed
var evtHandler =
function myeventHandler(evt) {
var cancel = false;
switch (evt.type) {
case "mousedown":
case "touchstart":
cancel = beginDrag(evt);
break;
case "mousemove":
case "touchmove":
cancel = duringDrag(evt);
break;
case "mouseup":
case "touchend":
cancel = endDrag(evt);
break;
default:
//handleDefault(evt);
}
if (cancel) {
//Prevent the event e from bubbling up the DOM
if (evt.cancelable) {
if (evt.preventDefault) {
evt.preventDefault();
}
if (evt.cancelBubble != null) {
evt.cancelBubble = true;
}
}
}
};
//Events to subscribe to
var events = ['mousedown', 'touchstart', 'mousemove', 'touchmove', 'mouseup', 'touchend'];
//Subscribe events
events.forEach(function (evtName) {
chartInstance.canvas.addEventListener(evtName, evtHandler);
});
}
};
The handler above is initiated like this with an existing Chart.js object:
chartEventHandler.init(chartAcTune);
The beginDrag(evt), duringDrag(evt) and endDrag(evt) have the same basic function as in the d3 example above. Just returns true when wanting to consume the event and not pasing it on for document panning and similar.
Try it in this Fiddle using a touch screen. Unless you touch close to select a chart point the rest of the chart will be transparent to touch/mouse events and allow panning the page.
I am trying to display the mouse cursors of all the connected client screen on every client's screen. Something like this : http://www.moock.org/unity/clients/uCoop/uCoop.html
I am working on socket.io using node.js.
I tried drawing a circle on the cursor position on the screen using context.drawImage on mousemove but the cursor remains on the screen even after the mouse moves away and clearing the screen makes it slow. So I think, drawing on a canvas is not a perfect solution, I just need to emit the information of mouse co-ordinates to the client somehow. But I don't know how.
Client side code snippet:
socket.on('draw_cursor', function (data) {
var line = data.line;
context.beginPath();
context.fillStyle = "#000000";
context.arc(line[0].x*width, line[0].y*height, 10, 0, 2*Math.PI);
context.fill();
delay(2000);
});
function mainLoop() {
if (mouse.move && mouse.pos_prev) {
// send line to to the server
socket.emit('draw_cursor', { line: [ mouse.pos, mouse.pos_prev ] });
}
}
Server side code snippet:
socket.on('draw_cursor', function (data) {
io.emit('draw_cursor', { line: data.line });
});
Thanks
Vinni
I propose you draw HTML elements instead of using a canvas. That way, you can reuse the same element for each cursor and just update the coordinates. To do this, you should add an ID to each draw_cursor message, to keep track of which element is which:
socket.on('draw_cursor', function (data) {
io.emit('draw_cursor', { line: data.line, id: socket.id });
});
Then, in your client handler, you find or create the HTML element and update it's position:
function getCursorElement (id) {
var elementId = 'cursor-' + id;
var element = document.getElementById(elementId);
if(element == null) {
element = document.createElement('div');
element.id = elementId;
element.className = 'cursor';
// Perhaps you want to attach these elements another parent than document
document.appendChild(element);
}
return element;
}
socket.on('draw_cursor', function (data) {
var el = getCursorElement(data.id);
el.style.x = data.line[0].x;
el.style.y = data.line[0].y;
}
Now, you just have to style the cursor elements. Here's a little css to start with:
.cursor {
position: absolute;
background: black;
width: 20px;
height: 20px;
border-radius: 10px;
}
I tried asking this question before but the way I asked it was so confusing that I didn't get any help. I originally thought it was React to blame for my touchmove events to ceasefire when updating subcomponents. I now am pretty sure it is the Chartist.js library, or possibly how I'm wrapping chartist into a react component, that is stopping the action.
Instead of rambling on about my question I've created two JSfiddles. One that shows you can create a React slider that updates it's values continuously, regardless of being called from mousemove or touchmove.
http://jsfiddle.net/higginsrob/uf6keps2/
// please follow the link for full example
The Second fiddle implements my react wrapper for chartist, and a simplified example of how I'm using it. When you click/drag on the chart it will select the data point at the current x value. This is working fine with a mouse, but trying it on mobile touch devices (or chrome's mobile emulator) it will only fire a few times, and only update the chart once.
http://jsfiddle.net/higginsrob/Lpcg1c6w/
// please follow the link for full example
Any help is appreciated!
Ok, so you need to put a transparent div in front of the chartist chart that captures the mousedown/touchstart, mousemove/touchmove, and mouseup/touchend events.
working example:
http://jsfiddle.net/higginsrob/jwhbzgrb/
// updated event functions:
onTouchStart: function (evt) {
evt.preventDefault();
this.is_touch = (evt.touches);
var node = evt.currentTarget.previousSibling;
var grid = node.querySelector('.ct-grids');
var bbox = grid.getBBox();
this.columnwidth = bbox.width / this.props.data.length;
this.offset = this.getScrollLeftOffset(node) + bbox.x + (this.columnwidth / 2);
this.istouching = true;
this.onTouchMove(evt);
}
onTouchMove: function (evt) {
if(this.istouching){
var x;
if (this.is_touch) {
if(evt.touches && evt.touches[0]){
x = evt.touches[0].clientX - this.offset;
}
} else {
x = evt.clientX - this.offset;
}
this.setState({
index: Math.round(x / this.columnwidth)
});
}
}
onTouchEnd: function(evt){
this.istouching = false;
}
// updated render function:
render: function () {
return React.DOM.div(
{
style: {
position: "relative"
}
},
ReactChartist({ ... your chartist chart here .... }),
// this div sits in front of the chart and captures events
React.DOM.div({
onMouseDown: this.onTouchStart,
onTouchStart: this.onTouchStart,
onMouseMove: this.onTouchMove,
onTouchMove: this.onTouchMove,
onMouseUp: this.onTouchEnd,
onTouchEnd: this.onTouchEnd,
style: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0
}
})
);
}
I've a script, which manipulates with canvas. I call it like this:
namespace.module.do(canvas, paramString);
I'd like to move the call to the canvas itself, so it'll somewhat similar to:
<canvas namespace.module.dobehavior=paramString />
Can I do it with JS and if not how close can I get to it?
The canvas is a DOM object, so you could attach functions to it. In your example:
document.getElementsByTagName("canvas")[0].namespace.module.dobehavior = function(paramString) { ... code ... };
(obviously, document.getElementsByTagName("canvas")[0].namespace and document.getElementsByTagName("canvas")[0].namespace.module would need to be an object, so you might need to do
document.getElementsByTagName("canvas")[0].namespace = { module : { dobehavior : function(paramString) { ... } } };
And then if you had the canvas element somewhere (possibly also through var canvas = document.getElementsByTagName("canvas")[0];) you could do
canvas.namespace.module.dobehavior("someString");
Is that what you mean?
In JQuery
$(function() {
$("canvas[ns.mod.colorBehavior]") {
var color = $(this).attr("ns.mod.colorBehavior");
if (color == "red") {
var context = this.getContext('2d');
context.fillStyle = "#ff0000";
context.fillRect(0,0,width,height);
}
}
}
Where width and heigh are the width and height of your canvas element (or a higher value like 10000).