fabric.js arrow snapped to grid - javascript

I've adapted some code to snap a line to a grid using Fabric.js, as in the code below. What I want is for it to instead be an arrow. I can't seem to get the arrowhead (triangle) to move properly. I kept breaking the code by trying to add a triangle as the arrowhead, so I removed all code attempting to add it. Please help me add the arrowhead, thank you!!
var canvas = new fabric.Canvas('c', { selection: false });
var grid = 50;
for (var i = 0; i < (600 / grid); i++) {
canvas.add(new fabric.Line([ i * grid, 0, i * grid, 600], { stroke: '#ccc', selectable: false }));
canvas.add(new fabric.Line([ 0, i * grid, 600, i * grid], { stroke: '#ccc', selectable: false }))
}
var line, isDown;
canvas.on('mouse:down', function(o){
canvas.remove(line);
isDown = true;
var pointer = canvas.getPointer(o.e);
var points = [ Math.round(pointer.x / grid) * grid, Math.round(pointer.y / grid) * grid, pointer.x, pointer.y ];
line = new fabric.Line(points, {
strokeWidth: 5,
fill: 'red',
stroke: 'red',
originX: 'center',
originY: 'center'
});
canvas.add(line);
});
canvas.on('mouse:move', function(o){
if (!isDown) return;
var pointer = canvas.getPointer(o.e);
line.set({ x2: pointer.x, y2: pointer.y });
canvas.renderAll();
});
canvas.on('mouse:up', function(o){
var pointer = canvas.getPointer(o.e);
isDown = false;
line.set({ x2: Math.round(pointer.x/ grid) * grid, y2: Math.round(pointer.y/ grid) * grid });
canvas.renderAll();
});
<script src="https://rawgithub.com/kangax/fabric.js/master/dist/fabric.js"></script>
<canvas id="c" width="500" height="500" style="border:1px solid #ccc"></canvas>

Got 'em! For me, the trick was defining the center of the triangle following the method in this fiddle: http://jsfiddle.net/ug2gskj1/
left: line.get('x1') + deltaX;
top: line.get('y1') + deltaY;
where
centerX = (line.x1 + line.x2) / 2;
centerY = (line.y1 + line.y2) / 2;
deltaX = line.left - centerX;
deltaY = line.top - centerY;
window.onload = function() {
var canvas = new fabric.Canvas('c', {
selection: false
});
var grid = 50;
for (var i = 0; i < (600 / grid); i++) {
canvas.add(new fabric.Line([i * grid, 0, i * grid, 600], {
stroke: '#ccc',
selectable: false
}));
canvas.add(new fabric.Line([0, i * grid, 600, i * grid], {
stroke: '#ccc',
selectable: false
}))
}
var line, triangle, isDown;
function calcArrowAngle(x1, y1, x2, y2) {
var angle = 0,
x, y;
x = (x2 - x1);
y = (y2 - y1);
if (x === 0) {
angle = (y === 0) ? 0 : (y > 0) ? Math.PI / 2 : Math.PI * 3 / 2;
} else if (y === 0) {
angle = (x > 0) ? 0 : Math.PI;
} else {
angle = (x < 0) ? Math.atan(y / x) + Math.PI : (y < 0) ? Math.atan(y / x) + (2 * Math.PI) : Math.atan(y / x);
}
return (angle * 180 / Math.PI + 90);
}
canvas.on('mouse:down', function(o) {
canvas.remove(line, triangle);
isDown = true;
var pointer = canvas.getPointer(o.e);
var points = [Math.round(pointer.x / grid) * grid, Math.round(pointer.y / grid) * grid, pointer.x, pointer.y];
line = new fabric.Line(points, {
strokeWidth: 5,
fill: 'red',
stroke: 'red',
originX: 'center',
originY: 'center'
});
centerX = (line.x1 + line.x2) / 2;
centerY = (line.y1 + line.y2) / 2;
deltaX = line.left - centerX;
deltaY = line.top - centerY;
triangle = new fabric.Triangle({
left: line.get('x1') + deltaX,
top: line.get('y1') + deltaY,
originX: 'center',
originY: 'center',
hasBorders: false,
hasControls: false,
lockScalingX: true,
lockScalingY: true,
lockRotation: true,
pointType: 'arrow_start',
angle: -45,
width: 20,
height: 20,
fill: 'red'
});
canvas.add(line, triangle);
});
canvas.on('mouse:move', function(o) {
//function angle(x1,y1,x2,y2){angle=Math.atan((y2-y1)/(x2-x1))*180/Math.PI+90; return angle;}
if (!isDown) return;
var pointer = canvas.getPointer(o.e);
line.set({
x2: pointer.x,
y2: pointer.y
});
triangle.set({
'left': pointer.x + deltaX,
'top': pointer.y + deltaY,
'angle': calcArrowAngle(line.x1, line.y1, line.x2, line.y2)
});
canvas.renderAll();
});
canvas.on('mouse:up', function(o) {
var pointer = canvas.getPointer(o.e);
isDown = false;
snappedxCoordinate = Math.round(pointer.x / grid) * grid;
snappedyCoordinate = Math.round(pointer.y / grid) * grid;
snappedxCoordinateArrowhead = Math.round((pointer.x + deltaX) / grid) * grid;
snappedyCoordinateArrowhead = Math.round((pointer.y + deltaY) / grid) * grid;
line.set({
x2: snappedxCoordinate,
y2: snappedyCoordinate
});
triangle.set({
'left': snappedxCoordinateArrowhead,
'top': snappedyCoordinateArrowhead,
'angle': calcArrowAngle(line.x1, line.y1, line.x2, line.y2)
});
canvas.renderAll();
});
}
<script src="https://rawgithub.com/kangax/fabric.js/master/dist/fabric.js"></script>
<canvas id="c" width="500" height="500" style="border:1px solid #ccc"></canvas>

Related

how to add random circles with no overlap inside a fabricjs canvas

I am trying to come up with an algorithm of adding random circles with no overlap inside fabricj canvas;
The bellow snippet is what i have tried.
const canvas = new fabric.Canvas('gameCanvas', {selection: false});
fabric.Object.prototype.originX = fabric.Object.prototype.originY = 'center';
let rectangle;
document.addEventListener('DOMContentLoaded', function(){
rectangle = new fabric.Rect({
width: 250,
height: 100,
fill: 'blue',
opacity: 0.2,
left: 200,
top: 140
});
canvas.add(rectangle);
});
document.getElementById('addCirclesBtn').addEventListener('click', function(){
drawCircles();
});
function drawCircles()
{
for(let i = 1; i <=20; i++)
{
let x= Math.random() * (rectangle.width - rectangle.left) + rectangle.left;
let y = Math.random() * (rectangle.height - rectangle.top) + rectangle.top;
let circle = new fabric.Circle({
left: x,
top: y,
strokeWidth: 2,
radius: 10,
fill: 'white',
stroke: '#666',
selectable: false,
hoverCursor: 'default',
hasControls: false,
hasBorders: false
});
canvas.add(circle);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.0.0/fabric.min.js"></script>
<canvas id="gameCanvas" width="500" height="300" style="border: 2px solid green;"></canvas>
<button id="addCirclesBtn">ADD CIRCLES</button>
This is what i came up with and the main logic is in the function drawCircles() which checks is circles are overlapping before adding inside the rectangle. It is to be noted that the program add 100 circles inside the rectangle
const canvas = new fabric.Canvas('gameCanvas', {selection: false});
fabric.Object.prototype.originX = fabric.Object.prototype.originY = 'center';
let rectangle;
const radius = 10;
let min_x;
let min_y;
let max_x;
let max_y;
let circles;
document.addEventListener('DOMContentLoaded', function(){
drawRectangle();
min_x = rectangle.left - rectangle.width / 2 ;
//min_x = 250 - 200/2;
max_x = rectangle.left + rectangle.width / 2;
//max_x = 250 + 200/2;
min_y = rectangle.top - rectangle.height / 2 ;
//min_y = 150 - 100/2;
max_y = rectangle.top + rectangle.height / 2;
//max_y = 150 + 100/2;
});
function drawRectangle()
{
rectangle = new fabric.Rect({
width: 200,
height: 100,
fill: 'blue',
opacity: 0.2,
left: 250,
top: 150
});
canvas.add(rectangle);
}
document.getElementById('addCirclesBtn').addEventListener('click', function(){
canvas.clear();
circles = [];
drawRectangle();
drawCircles();
});
function drawCircles()
{
let anti_crash_count = 0
while(circles.length < 100)
{
let circle = new fabric.Circle({
left: getRandomNumber(min_x + radius, max_x - radius),
top: getRandomNumber(min_y + radius, max_y - radius),
strokeWidth: 2,
radius: radius,
fill: 'white',
stroke: '#666',
selectable: false,
hoverCursor: 'default',
hasControls: false,
hasBorders: false,
originX:'center',
originY:'center'
});
let isOverlapping = false;
for(let j = 0; j < circles.length; j++)
{
let previous_circle = circles[j];
let distance = Math.hypot(circle.left - previous_circle.left, circle.top - previous_circle.top);
if(distance < (circle.radius + previous_circle.radius))
{
isOverlapping = true;
break;
}
}
if(!isOverlapping)
{
canvas.add(circle);
circles.push(circle);
}
anti_crash_count ++;
if(anti_crash_count > 1000)
{
break;
}
}
}
function getRandomNumber(min, max)
{
return Math.floor((Math.random() * (max - min)) + min);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.0.0/fabric.min.js"></script>
<canvas id="gameCanvas" width="500" height="300" style="border: 2px solid green;"></canvas>
<button id="addCirclesBtn">ADD CIRCLES</button>
I would appreciate any idea on how to improve it if necessary.

Draw an arrow that is always pointing away from an object

I have a canvas with a circle and a triangle with a line connecting them both. I'm attempting to use the triangle as an arrow. I'd like for the arrow to always point away from the center of the circle if either object is moved. I thought I could use the angle of the line to rotate the triangle but the results are unsatisfactory and I'm certain my approach is wrong.
Any suggestions or tips for solving this problem? In my linked JSFiddle you can move either object around and the angle of the triangle is rotated, but it's never quite right:
https://jsfiddle.net/zh1bkfs5/
const c = new fabric.Canvas('c');
c.preserveObjectStacking = true;
function radiansToDegrees (radians) {
return radians * 180 / Math.PI;
}
function calcAngle (opposite, adjacent) {
return radiansToDegrees(Math.atan(opposite / adjacent));
}
function syncObjects (e) {
const x1 = triangle.left;
const y1 = triangle.top;
const x2 = circle.left + circle.radius;
const y2 = circle.top + circle.radius;
line.set('x1', x1);
line.set('y1', y1);
line.set('x2', x2);
line.set('y2', y2);
const angle = calcAngle(y2 - y1, x1 - x2);
triangle.set('angle', -angle);
triangle.setCoords();
c.renderAll();
}
const circle = new fabric.Circle({
fill: 'black',
radius: 20,
top: 100,
left: 100,
hasBorders: false,
hasControls: false
});
const triangle = new fabric.Triangle({
fill: 'orange',
height: 20,
width: 20,
top: 50,
left: 120,
hasBorders: false,
hasControls: false,
originX: 'center',
originY: 'center'
});
const coords = [
triangle.left,
triangle.top,
circle.left + circle.radius,
circle.top + circle.radius
];
const line = new fabric.Line(coords, {
stroke: 'orange',
strokeWidth: 1,
selectable: false
});
c.add(circle, line, triangle);
c.on('object:moving', syncObjects);
Change the part of your syncObject function to this:
const x = x1 - x2;
const y = y2 - y1;
const angle = calcAngle(x, y);
if(x < 0 && y > 0 || x > 0 && y > 0) {
triangle.set('angle', angle);
} else if(x < 0 && y < 0) {
triangle.set('angle', 180 + angle);
} if(x > 0 && y < 0) {
triangle.set('angle', -180 + angle);
}
this should work. Your updated fiddle https://jsfiddle.net/zh1bkfs5/30/
I had my the values for "adjacent" and "opposite" flipped, after updating that I no longer needed to invert the angle. I'm now 99% there, just have a case where the triangle is sometimes flipped the wrong way but should be easy to fix at least my initial approach worked.
Update: https://jsfiddle.net/zh1bkfs5/1/
const c = new fabric.Canvas('c');
c.preserveObjectStacking = true;
function radiansToDegrees (radians) {
return radians * 180 / Math.PI;
}
function calcAngle (opposite, adjacent) {
return radiansToDegrees(Math.atan(opposite / adjacent));
}
function syncObjects (e) {
const x1 = triangle.left;
const y1 = triangle.top;
const x2 = circle.left + circle.radius;
const y2 = circle.top + circle.radius;
line.set('x1', x1);
line.set('y1', y1);
line.set('x2', x2);
line.set('y2', y2);
const angle = calcAngle(x1 - x2, y2 - y1);
triangle.set('angle', angle);
triangle.setCoords();
c.renderAll();
}
const circle = new fabric.Circle({
fill: 'black',
radius: 20,
top: 100,
left: 100,
hasBorders: false,
hasControls: false
});
const triangle = new fabric.Triangle({
fill: 'orange',
height: 20,
width: 20,
top: 50,
left: 120,
hasBorders: false,
hasControls: false,
originX: 'center',
originY: 'center'
});
const coords = [
triangle.left,
triangle.top,
circle.left + circle.radius,
circle.top + circle.radius
];
const line = new fabric.Line(coords, {
stroke: 'orange',
strokeWidth: 1,
selectable: false
});
c.add(circle, line, triangle);
c.on('object:moving', syncObjects);

JavaScript FabricJs put image to canvas view

I want to create a circle and an arrow form using Fabric.js, so that for instance if one clicks on the arrow button, one should be able to draw an arrow (with the current selected width and height), same for the circle. The regarding html Looks like this:
<div class="btn-group-toggle toolbar-left-btns" data-toggle="buttons">
<h4 class="h4-color-white" id="current-shape-name" style="display:none;"></h4>
<label class="btn btn-secondary btn-md btn-md btn-tool-margin-left" title="Create an arrow object" id="drawing-arrow-shape">
<input type="radio" name="drawing-shape">
<i class="fas fa-arrow-right"></i>
</label>
<label class="btn btn-secondary btn-md btn-tool-margin-left" title="Create a circle object" id="drawing-circle-shape">
<input type="radio" name="drawing-shape">
<i class="far fa-circle"></i>
</label>
</div>
Basically a selection if one uses the arrow or circle form. I won't post the full script since ist really Long, currently I am able to select a Color and a brush width and paint on my canvas. That works, so Keep that in mind. I am just not "able" to create a circle and an arrow. In the following part of my script I am selecting of the if its an arrow or circle (the selection works) and if selected to "draw" that arrow or circle in my canvas, it draws Right after selecting a mix between an arrow and a cube, any idea what I am missing. Nor do I get an error message.
const canvasRenderer = {
render() {
this.initCanvas();
},
elements: {
...
canvas: '',
isDown: null,
circle: null,
circleOrigX: null,
circleOrigY: null,
startX: null,
startY: null,
id: {
...
drawingArrowShape: $('#drawing-arrow-shape'),
drawingCircleShape: $('#drawing-circle-shape'),
imgInput: $('#img-input'),
},
tags: {
html: $('html')
}
},
propToolRadio() {
this.elements.id.drawingArrowShape.prop('checked', false);
this.elements.id.drawingCircleShape.prop('checked', false);
this.elements.id.drawingArrowShape.parent().removeClass('active');
this.elements.id.drawingCircleShape.parent().removeClass('active');
},
recOnArrow(canvas) {
this.elements.id.drawingArrowShape.change((e) => {
canvas.isDrawingMode = false;
this.elements.id.currentShapeName.innerHTML = 'Arrow';
canvas.off('mouse:down', this.onCircleMouseDown(e));
canvas.off('mouse:move', this.onCircleMouseMove(e));
canvas.off('mouse:up', this.onCircleMouseUp(e));
canvas.on('mouse:down', this.onArrowMouseDown(e));
canvas.on('mouse:move', this.onArrowMouseMove(e));
canvas.on('mouse:up', this.onArrowMouseUp(e));
this.recCanvas(canvas);
});
},
recOnCircle(canvas) {
this.elements.id.drawingCircleShape.change((e) => {
canvas.isDrawingMode = false;
this.elements.id.currentShapeName.innerHTML = 'Circle';
canvas.on('mouse:down', this.onCircleMouseDown(e));
canvas.on('mouse:move', this.onCircleMouseMove(e));
canvas.on('mouse:up', this.onCircleMouseUp(e));
canvas.off('mouse:down', this.onArrowMouseDown(e));
canvas.off('mouse:move', this.onArrowMouseMove(e));
canvas.off('mouse:up', this.onArrowMouseUp(e));
this.recCanvas(canvas);
});
},
onArrowMouseDown(o) {
let pointer = this.elements.canvas.getPointer(o.e);
this.elements.startX = pointer.x;
this.elements.startY = pointer.y;
this.recCanvas(this.elements.canvas);
},
onArrowMouseUp(o) {
let pointer = this.elements.canvas.getPointer(o.e);
let endX = pointer.x;
let endY = pointer.y;
this.showArrow(
this.elements.startX,
this.elements.startY,
endX,
endY,
this.elements.canvas
);
this.recCanvas(this.elements.canvas);
},
onArrowMouseMove(e) {
},
onCircleMouseDown(o) {
this.elements.isDown = true;
let pointer = this.elements.canvas.getPointer(o.e);
this.elements.circleOrigX = pointer.x;
this.elements.circleOrigY = pointer.y;
if (!this.elements.circle) {
this.elements.circle = new fabric.Circle({
left: this.elements.circleOrigX,
top: this.elements.circleOrigY,
originX: 'center',
originY: 'center',
radius: 0,
fill: '',
stroke: this.elements.id.drawingColorEl.value,
strokeWidth: parseInt(this.elements.id.drawingLineWidthEl.value, 10) || 1,
selectable: true
});
this.elements.canvas.add(this.elements.circle);
}
this.recCanvas(this.elements.canvas);
},
onCircleMouseMove(o) {
console.log("onCircleMouseMove");
if (!this.elements.isDown) return;
let pointer = this.elements.canvas.getPointer(o.e);
this.elements.circle.set({
radius: Math.sqrt(
Math.pow(
(this.elements.circleOrigX - pointer.x), 2
) + Math.pow((this.elements.circleOrigY - pointer.y), 2)
)
});
this.elements.canvas.renderAll();
},
onCircleMouseUp(o) {
console.log("onCircleMouseUp");
this.elements.isDown = false;
this.elements.circle = null;
},
showArrow(fromx, fromy, tox, toy, canvas) {
console.log("showArrow");
let angle = Math.atan2(toy - fromy, tox - fromx);
let headlen = 15; // arrow head size
// bring the line end back some to account for arrow head.
tox = tox - (headlen) * Math.cos(angle);
toy = toy - (headlen) * Math.sin(angle);
// calculate the points.
let points = [{
x: fromx, // start point
y: fromy
}, {
x: fromx - (headlen / 4) * Math.cos(angle - Math.PI / 2),
y: fromy - (headlen / 4) * Math.sin(angle - Math.PI / 2)
}, {
x: tox - (headlen / 4) * Math.cos(angle - Math.PI / 2),
y: toy - (headlen / 4) * Math.sin(angle - Math.PI / 2)
}, {
x: tox - (headlen) * Math.cos(angle - Math.PI / 2),
y: toy - (headlen) * Math.sin(angle - Math.PI / 2)
}, {
x: tox + (headlen) * Math.cos(angle), // tip
y: toy + (headlen) * Math.sin(angle)
}, {
x: tox - (headlen) * Math.cos(angle + Math.PI / 2),
y: toy - (headlen) * Math.sin(angle + Math.PI / 2)
}, {
x: tox - (headlen / 4) * Math.cos(angle + Math.PI / 2),
y: toy - (headlen / 4) * Math.sin(angle + Math.PI / 2)
}, {
x: fromx - (headlen / 4) * Math.cos(angle + Math.PI / 2),
y: fromy - (headlen / 4) * Math.sin(angle + Math.PI / 2)
}, {
x: fromx,
y: fromy
}];
let pline = new fabric.Polyline(points, {
fill: 'black',
stroke: this.elements.id.drawingColorEl.value,
opacity: 1,
// strokeWidth: 2,
strokeWidth: parseInt(this.elements.id.drawingLineWidthEl.value, 10) || 1,
originX: 'left',
originY: 'top',
selectable: true
});
canvas.add(pline);
canvas.renderAll();
},
/**
* TODO on mdified, moving, rotating, scaling method to call only once
*/
recOnEvent(canvas) {
let isObjectMoving = false;
canvas.on('path:created', () => {
this.recCanvas(canvas)
});
canvas.on('object.added', () => {
this.recCanvas(canvas)
});
canvas.on('object:modified', () => {
isObjectMoving = true;
});
canvas.on('object:moving', () => {
isObjectMoving = true;
});
canvas.on('object:removed', () => {
this.recCanvas(canvas)
});
canvas.on('object:rotating', () => {
isObjectMoving = true;
});
canvas.on('object:scaling', () => {
isObjectMoving = true;
});
this.elements.id.canvasContainer.click(() => {
this.recCanvas(canvas);
});
canvas.on('mouse:up', () => {
if (isObjectMoving) {
isObjectMoving = false;
this.recCanvas(canvas); // fire this if finished
}
});
},
};
$(document).ready(() => {
canvasRenderer.render();
});

Fabric.js - wrong group position

I would like add arrow to my canvas - user click on canvas, fabric draw line + triangle. Then, when user move mouse line is "moving" and arrow angle is changed. "Drawing mode" is end when mouse up. This works great, but positions and selection area are wrong.
I write that for mouse:down:
// Draw new arrow using move
canvas.fabric.on('mouse:down', function(options){
$scope.lineOptions = {
stroke: '#000',
selectable: true,
strokeWidth: '2',
padding: 5,
hasBorders: false,
hasControls: false,
//originX: 'top',
//originY: 'left',
lockScalingX: true,
lockScalingY: true
};
var zoomFactor = Math.pow(canvas.currentZoom, -1);
var x = (options.e.clientX - canvas.fabric._offset.left) * zoomFactor;
var y = (options.e.clientY - canvas.fabric._offset.top) * zoomFactor;
var points = [ 0, 0, 0, 0 ];
temp_line = new fabric.Line(points, $scope.lineOptions);
temp_arrow = new fabric.Triangle({
left: x,
top: y,
angle: -45,
originX: 'center',
originY: 'center',
width: 20,
height: 20,
fill: '#000'
});
arrowShape = new fabric.Group([ temp_line, temp_arrow ], {
left: x,
top: y
});
canvas.fabric.add(arrowShape);
});
Zoom factor is here negligible.
There is code for mouse:move:
canvas.fabric.on('mouse:move', function(o){
var pointer = canvas.fabric.getPointer(o.e);
arrowShape.item(0).set({ x2: pointer.x, y2: pointer.y });
arrowShape.item(1).set({ left: pointer.x, top: pointer.y });
x1 = arrowShape.item(0).get('x1');
y1 = arrowShape.item(0).get('y1');
x2 = arrowShape.item(0).get('x2');
y2 = arrowShape.item(0).get('y2');
angle = calcArrowAngle(x1, y1, x2, y2);
arrowShape.item(1).set('angle', angle + 90);
//arrowShape.item(0).setCoords();
//arrowShape.item(1).setCoords();
//arrowShape.setCoords();
canvas.fabric.renderAll();
});
function calcArrowAngle(x1, y1, x2, y2) {
var angle = 0,
x, y;
x = (x2 - x1);
y = (y2 - y1);
if (x === 0) {
angle = (y === 0) ? 0 : (y > 0) ? Math.PI / 2 : Math.PI * 3 / 2;
} else if (y === 0) {
angle = (x > 0) ? 0 : Math.PI;
} else {
angle = (x < 0) ? Math.atan(y / x) + Math.PI : (y < 0) ? Math.atan(y / x) + (2 * Math.PI) : Math.atan(y / x);
}
return (angle * 180 / Math.PI);
}
and for mouse:up:
canvas.fabric.on('mouse:up', function(o){
canvas.fabric.setActiveObject(arrowShape);
// My drawing mode
$scope.drawMode = false;
});
Adding works great, change direction works great, change angle works great... but group selection and line start point is wrong:
OriginX/originY are default, so they should be top/left.. but the aren't. Also, group selection area are wrong, too small for both shapes. I forgot about something... but about what?

kineticjs performance lag

I am working on a radial control similar to the HTML5 wheel of fortune example. I've modified the original here with an example of some additional functionality I require: http://jsfiddle.net/fEm9P/ When you click on the inner kinetic wedges they will shrink and expand within the larger wedges. Unfortunately when I rotate the wheel it lags behind the pointer. It's not too bad here but it's really noticeable on a mobile.
I know this is due to the fact that I'm not caching the wheel. When I do cache the wheel (uncomment lines 239-249) the inner wedges no longer respond to mouse/touch but the response on rotation is perfect. I have also tried adding the inner wedges to a separate layer and caching the main wheel only. I then rotate the inner wheel with the outer one. Doing it this way is a little better but still not viable on mobile.
Any suggestions would be greatly appreciated.
Stephen
//constants
var MAX_ANGULAR_VELOCITY = 360 * 5;
var NUM_WEDGES = 25;
var WHEEL_RADIUS = 410;
var ANGULAR_FRICTION = 0.2;
// globals
var angularVelocity = 360;
var lastRotation = 0;
var controlled = false;
var target, activeWedge, stage, layer, wheel,
pointer, pointerTween, startRotation, startX, startY;
var currentVolume, action;
function purifyColor(color) {
var randIndex = Math.round(Math.random() * 3);
color[randIndex] = 0;
return color;
}
function getRandomColor() {
var r = 100 + Math.round(Math.random() * 55);
var g = 100 + Math.round(Math.random() * 55);
var b = 100 + Math.round(Math.random() * 55);
var color = [r, g, b];
color = purifyColor(color);
color = purifyColor(color);
return color;
}
function bind() {
wheel.on('mousedown', function(evt) {
var mousePos = stage.getPointerPosition();
angularVelocity = 0;
controlled = true;
target = evt.targetNode;
startRotation = this.rotation();
startX = mousePos.x;
startY = mousePos.y;
});
// add listeners to container
document.body.addEventListener('mouseup', function() {
controlled = false;
action = null;
if(angularVelocity > MAX_ANGULAR_VELOCITY) {
angularVelocity = MAX_ANGULAR_VELOCITY;
}
else if(angularVelocity < -1 * MAX_ANGULAR_VELOCITY) {
angularVelocity = -1 * MAX_ANGULAR_VELOCITY;
}
angularVelocities = [];
}, false);
document.body.addEventListener('mousemove', function(evt) {
var mousePos = stage.getPointerPosition();
var x1, y1;
if(action == 'increase') {
x1 = (mousePos.x-(stage.getWidth() / 2));
y1 = (mousePos.y-WHEEL_RADIUS+20);
var r = Math.sqrt(x1 * x1 + y1 * y1);
if (r>500){
r=500;
} else if (r<100){
r=100;
};
currentVolume.setRadius(r);
layer.draw();
} else {
if(controlled && mousePos && target) {
x1 = mousePos.x - wheel.x();
y1 = mousePos.y - wheel.y();
var x2 = startX - wheel.x();
var y2 = startY - wheel.y();
var angle1 = Math.atan(y1 / x1) * 180 / Math.PI;
var angle2 = Math.atan(y2 / x2) * 180 / Math.PI;
var angleDiff = angle2 - angle1;
if ((x1 < 0 && x2 >=0) || (x2 < 0 && x1 >=0)) {
angleDiff += 180;
}
wheel.setRotation(startRotation - angleDiff);
}
};
}, false);
}
function getRandomReward() {
var mainDigit = Math.round(Math.random() * 9);
return mainDigit + '\n0\n0';
}
function addWedge(n) {
var s = getRandomColor();
var reward = getRandomReward();
var r = s[0];
var g = s[1];
var b = s[2];
var angle = 360 / NUM_WEDGES;
var endColor = 'rgb(' + r + ',' + g + ',' + b + ')';
r += 100;
g += 100;
b += 100;
var startColor = 'rgb(' + r + ',' + g + ',' + b + ')';
var wedge = new Kinetic.Group({
rotation: n * 360 / NUM_WEDGES,
});
var wedgeBackground = new Kinetic.Wedge({
radius: WHEEL_RADIUS,
angle: angle,
fillRadialGradientStartRadius: 0,
fillRadialGradientEndRadius: WHEEL_RADIUS,
fillRadialGradientColorStops: [0, startColor, 1, endColor],
fill: '#64e9f8',
fillPriority: 'radial-gradient',
stroke: '#ccc',
strokeWidth: 2,
rotation: (90 + angle/2) * -1
});
wedge.add(wedgeBackground);
var text = new Kinetic.Text({
text: reward,
fontFamily: 'Calibri',
fontSize: 50,
fill: 'white',
align: 'center',
stroke: 'yellow',
strokeWidth: 1,
listening: false
});
text.offsetX(text.width()/2);
text.offsetY(WHEEL_RADIUS - 15);
wedge.add(text);
volume = createVolumeControl(angle, endColor);
wedge.add(volume);
wheel.add(wedge);
}
var activeWedge;
function createVolumeControl(angle, colour){
var volume = new Kinetic.Wedge({
radius: 100,
angle: angle,
fill: colour,
stroke: '#000000',
rotation: (90 + angle/2) * -1
});
volume.on("mousedown touchstart", function() {
currentVolume = this;
action='increase';
});
return volume;
}
function animate(frame) {
// wheel
var angularVelocityChange = angularVelocity * frame.timeDiff * (1 - ANGULAR_FRICTION) / 1000;
angularVelocity -= angularVelocityChange;
if(controlled) {
angularVelocity = ((wheel.getRotation() - lastRotation) * 1000 / frame.timeDiff);
}
else {
wheel.rotate(frame.timeDiff * angularVelocity / 1000);
}
lastRotation = wheel.getRotation();
// pointer
var intersectedWedge = layer.getIntersection({x: stage.width()/2, y: 50});
if (intersectedWedge && (!activeWedge || activeWedge._id !== intersectedWedge._id)) {
pointerTween.reset();
pointerTween.play();
activeWedge = intersectedWedge;
}
}
function init() {
stage = new Kinetic.Stage({
container: 'container',
width: 578,
height: 500
});
layer = new Kinetic.Layer();
wheel = new Kinetic.Group({
x: stage.getWidth() / 2,
y: WHEEL_RADIUS + 20
});
for(var n = 0; n < NUM_WEDGES; n++) {
addWedge(n);
}
pointer = new Kinetic.Wedge({
fillRadialGradientStartPoint: 0,
fillRadialGradientStartRadius: 0,
fillRadialGradientEndPoint: 0,
fillRadialGradientEndRadius: 30,
fillRadialGradientColorStops: [0, 'white', 1, 'red'],
stroke: 'white',
strokeWidth: 2,
lineJoin: 'round',
angle: 30,
radius: 30,
x: stage.getWidth() / 2,
y: 20,
rotation: -105,
shadowColor: 'black',
shadowOffset: {x:3,y:3},
shadowBlur: 2,
shadowOpacity: 0.5
});
// add components to the stage
layer.add(wheel);
layer.add(pointer);
stage.add(layer);
pointerTween = new Kinetic.Tween({
node: pointer,
duration: 0.1,
easing: Kinetic.Easings.EaseInOut,
y: 30
});
pointerTween.finish();
var radiusPlus2 = WHEEL_RADIUS + 2;
wheel.cache({
x: -1* radiusPlus2,
y: -1* radiusPlus2,
width: radiusPlus2 * 2,
height: radiusPlus2 * 2
}).offset({
x: radiusPlus2,
y: radiusPlus2
});
layer.draw();
// bind events
bind();
var anim = new Kinetic.Animation(animate, layer);
//document.getElementById('debug').appendChild(layer.hitCanvas._canvas);
// wait one second and then spin the wheel
setTimeout(function() {
anim.start();
}, 1000);
}
init();
I made a couple of changes to the script which greatly improved the response time. The first was replacing layer.draw() with layer.batchDraw(). As the draw function was being called on each touchmove event it was making the interaction clunky. BatchDraw on the other hand will stack up draw requests internally "limit the number of redraws per second based on the maximum number of frames per second" (http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-batch-draw).
The jumping around of the canvas I seeing originally when I cached/cleared the wheel was due to the fact that I wasn't resetting the offset on the wheel when I cleared the cache.
http://jsfiddle.net/leydar/a7tkA/5
wheel.clearCache().offset({
x: 0,
y: 0
});
I hope this is of benefit to someone else. It's still not perfectly responsive but it's at least going in the right direction.
Stephen

Categories

Resources