I'm trying to create an arrow shape using fabricjs. Thus far my best approach has been to add a line and a triangle and combine them into a composite group. The problem however is, when I resize the arrow, the arrow head gets stretched and its not a nice effect.
What I'm asking is, how would you go about creating an arrow object on fabricjs that can be resized lengthwise only without stretching the arrow head.
http://jsfiddle.net/skela/j45czqge/
<html>
<head>
<script src='http://fabricjs.com/build/files/text,gestures,easing,parser,freedrawing,interaction,serialization,image_filters,gradient,pattern,shadow,node.js'></script> <meta charset="utf-8">
<style>
html,body
{
height: 100%; min-height:100%;
width: 100%; min-width:100%;
background-color:transparent;
margin:0;
}
button
{
height:44px;
margin:0;
}
</style>
</head>
<body>
<span id="dev">
<button id="draw_mode" onclick="toggleDraw()">Draw</button>
<button onclick="addRect()">Add Rect</button>
<button onclick="addCircle()">Add Circle</button>
<button onclick="addTriangle()">Add Triangle</button>
<button onclick="addLine()">Add Line</button>
<button onclick="addArrow()">Add Arrow</button>
<button onclick="clearCanvas()">Clear</button>
<button onclick="saveCanvas()">Save</button>
<button onclick="loadCanvas()">Load</button>
</span>
<span id="selected" style="visibility:hidden;">
<button onclick="removeSelected()">Remove</button>
</span>
<canvas id="c" style="border:1px solid #aaa;"></canvas>
<script>
fabric.Object.prototype.toObject = (function (toObject)
{
return function ()
{
return fabric.util.object.extend(toObject.call(this),
{
id:this.id,
});
};
})(fabric.Object.prototype.toObject);
fabric.LineArrow = fabric.util.createClass(fabric.Line, {
type: 'lineArrow',
initialize: function(element, options) {
options || (options = {});
this.callSuper('initialize', element, options);
},
toObject: function() {
return fabric.util.object.extend(this.callSuper('toObject'));
},
_render: function(ctx){
this.callSuper('_render', ctx);
// do not render if width/height are zeros or object is not visible
if (this.width === 0 || this.height === 0 || !this.visible) return;
ctx.save();
var xDiff = this.x2 - this.x1;
var yDiff = this.y2 - this.y1;
var angle = Math.atan2(yDiff, xDiff);
ctx.translate((this.x2 - this.x1) / 2, (this.y2 - this.y1) / 2);
ctx.rotate(angle);
ctx.beginPath();
//move 10px in front of line to start the arrow so it does not have the square line end showing in front (0,0)
ctx.moveTo(10,0);
ctx.lineTo(-20, 15);
ctx.lineTo(-20, -15);
ctx.closePath();
ctx.fillStyle = this.stroke;
ctx.fill();
ctx.restore();
}
});
fabric.LineArrow.fromObject = function (object, callback) {
callback && callback(new fabric.LineArrow([object.x1, object.y1, object.x2, object.y2],object));
};
fabric.LineArrow.async = true;
var canvas = new fabric.Canvas('c');
canvas.isDrawingMode = false;
canvas.freeDrawingBrush.width = 5;
setColor('red');
var sendToApp = function(_key, _val)
{
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", _key + ":##drawings##" + _val);
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
};
canvas.on('object:selected',function(options)
{
if (options.target)
{
//console.log('an object was selected! ', options.target.type);
var sel = document.getElementById("selected");
sel.style.visibility = "visible";
sendToApp("object:selected","");
}
});
canvas.on('selection:cleared',function(options)
{
//console.log('selection cleared');
var sel = document.getElementById("selected");
sel.style.visibility = "hidden";
sendToApp("selection:cleared","");
});
canvas.on('object:modified',function(options)
{
if (options.target)
{
//console.log('an object was modified! ', options.target.type);
sendToApp("object:modified","");
}
});
canvas.on('object:added',function(options)
{
if (options.target)
{
if (typeof options.target.id == 'undefined')
{
options.target.id = 1337;
}
//console.log('an object was added! ', options.target.type);
sendToApp("object:added","");
}
});
canvas.on('object:removed',function(options)
{
if (options.target)
{
//console.log('an object was removed! ', options.target.type);
sendToApp("object:removed","");
}
});
window.addEventListener('resize', resizeCanvas, false);
function resizeCanvas()
{
canvas.setHeight(window.innerHeight);
canvas.setWidth(window.innerWidth);
canvas.renderAll();
}
function color()
{
return canvas.freeDrawingBrush.color;
}
function setColor(color)
{
canvas.freeDrawingBrush.color = color;
}
function toggleDraw()
{
setDrawingMode(!canvas.isDrawingMode);
}
function setDrawingMode(isDrawingMode)
{
canvas.isDrawingMode = isDrawingMode;
var btn = document.getElementById("draw_mode");
btn.innerHTML = canvas.isDrawingMode ? "Drawing" : "Draw";
sendToApp("mode",canvas.isDrawingMode ? "drawing" : "draw");
}
function setLineControls(line)
{
line.setControlVisible("tr",false);
line.setControlVisible("tl",false);
line.setControlVisible("br",false);
line.setControlVisible("bl",false);
line.setControlVisible("ml",false);
line.setControlVisible("mr",false);
}
function createLine(points)
{
var line = new fabric.Line(points,
{
strokeWidth: 5,
stroke: color(),
originX: 'center',
originY: 'center',
lockScalingX:true,
//lockScalingY:false,
});
setLineControls(line);
return line;
}
function createArrowHead(points)
{
var headLength = 15,
x1 = points[0],
y1 = points[1],
x2 = points[2],
y2 = points[3],
dx = x2 - x1,
dy = y2 - y1,
angle = Math.atan2(dy, dx);
angle *= 180 / Math.PI;
angle += 90;
var triangle = new fabric.Triangle({
angle: angle,
fill: color(),
top: y2,
left: x2,
height: headLength,
width: headLength,
originX: 'center',
originY: 'center',
// lockScalingX:false,
// lockScalingY:true,
});
return triangle;
}
function addRect()
{
canvas.add(new fabric.Rect({left:100,top:100,fill:color(),width:50,height:50}));
}
function addCircle()
{
canvas.add(new fabric.Circle({left:150,top:150,fill:color(),radius:50/2}));
}
function addTriangle()
{
canvas.add(new fabric.Triangle({left:200,top:200,fill:color(),height:50,width:46}));
}
function addLine()
{
var line = createLine([100,100,100,200]);
canvas.add(line);
}
function addArrow()
{
var pts = [100,100,100,200];
var triangle = createArrowHead(pts);
var line = createLine(pts);
var grp = new fabric.Group([triangle,line]);
setLineControls(grp);
canvas.add(grp);
// var arrow = new fabric.LineArrow(pts,{left:100,top:100,fill:color()});
// setLineControls(arrow);
// canvas.add(arrow);
}
function removeSelected()
{
var grp = canvas.getActiveGroup();
var obj = canvas.getActiveObject();
if (obj!=null)
{
canvas.remove(obj);
}
if (grp!=null)
{
grp.forEachObject(function(o){ canvas.remove(o) });
canvas.discardActiveGroup().renderAll();
}
}
function clearCanvas()
{
canvas.clear();
}
function saveCanvas()
{
var js = JSON.stringify(canvas);
return js;
}
function loadCanvas()
{
var js = '{"objects":[{"type":"circle","originX":"left","originY":"top","left":150,"top":150,"width":50,"height":50,"fill":"red","stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over","id":1234,"radius":25,"startAngle":0,"endAngle":6.283185307179586}],"background":""}';
canvas.loadFromJSON(js);
}
resizeCanvas();
</script>
</body>
</html>
I had the same problem and ended up doing math to calculate the points that would make up an arrow shape around a line and using a polygon object instead.
The core of it looks like:
var angle = Math.atan2(toy - fromy, tox - fromx);
var 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.
var 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
}
];
Then create a polygon from the points.
https://jsfiddle.net/6e17oxc3/
What you can do is calculate the new size after the object is stretched and draw another object on the same exact area and removing the previous one.
var obj = canvas.getActiveObject();
var width = obj.getWidth();
var height = obj.getHeight;
var top = obj.getTop();
Now if you have only one object that is stretched, you can simply use the data above to draw another nicely looking object on the canvas.
If you have multiples then you need to get the data for all of them and draw them one by one.
Related
I am using this post answer and works fine to me.
The problem comes when I make a json with the canvas, and I try to set it on another canvas with loadFromJSON method.
I think the problem is that there is no fromObject method for this new subclass, and I have tryed to type it, but I cannot write anything that works.
This is how I defined the subclass (it is almost copy-paste from the link)
fabric.LineaBote = fabric.util.createClass(fabric.Line, {
type: 'linea_bote',
initialize: function (element, options) {
options || (options = {});
this.callSuper('initialize', element, options);
// Set default options
this.set({
hasBorders: false,
hasControls: false,
});
},
_render: function (ctx) {
this.callSuper('_render', ctx);
ctx.save();
const xDiff = this.x2 - this.x1;
const yDiff = this.y2 - this.y1;
const angle = Math.atan2(yDiff, xDiff);
ctx.translate(xDiff / 2, yDiff / 2);
ctx.rotate(angle);
ctx.beginPath();
ctx.closePath();
ctx.fillStyle = this.stroke;
ctx.fill();
ctx.restore();
var p = this.calcLinePoints();
var point = this.pointOnLine(this.point(p.x2, p.y2), this.point(p.x1, p.y1), 15)
this.wavy(this.point(p.x1, p.y1), point, this.point(p.x2, p.y2), ctx);
ctx.stroke();
},
point: function (x, y) {
return {
x: x,
y: y
};
},
wavy: function (from, to, endPoint, ctx) {
var cx = 0,
cy = 0,
fx = from.x,
fy = from.y,
tx = to.x,
ty = to.y,
i = 0,
step = 2,
waveOffsetLength = 0,
ang = Math.atan2(ty - fy, tx - fx),
distance = Math.sqrt((fx - tx) * (fx - tx) + (fy - ty) * (fy - ty)),
amplitude = -3,
f = Math.PI * distance / 10;
for (i; i <= distance; i += step) {
waveOffsetLength = Math.sin((i / distance) * f) * amplitude;
cx = from.x + Math.cos(ang) * i + Math.cos(ang - Math.PI / 2) * waveOffsetLength;
cy = from.y + Math.sin(ang) * i + Math.sin(ang - Math.PI / 2) * waveOffsetLength;
i > 0 ? ctx.lineTo(cx, cy) : ctx.moveTo(cx, cy);
}
ctx.lineTo(to.x, to.y);
ctx.lineTo(endPoint.x, endPoint.y);
},
pointOnLine: function (point1, point2, dist) {
var len = Math.sqrt(((point2.x - point1.x) * (point2.x - point1.x)) + ((point2.y - point1.y) * (point2.y - point1.y)));
var t = (dist) / len;
var x3 = ((1 - t) * point1.x) + (t * point2.x),
y3 = ((1 - t) * point1.y) + (t * point2.y);
return new fabric.Point(x3, y3);
},
toObject: function () {
return fabric.util.object.extend(this.callSuper('toObject'), {
customProps: this.customProps,
});
},});
And here is how I was trying to write fromObject() function:
fabric.LineaBote.fromObject = function (points, callback) {
callback && callback(new fabric.LineaBote(points, object));};
Error from google chorme console: Uncaught TypeError: Cannot read property 'fromObject' of undefined
As I thougth, the problem was that fromObject was not defined. The author updated the original post answer and wrote the function. Thanks to him.
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();
});
I have a circle, and a object.
I want to draw a circle segment with specified spread, and next check that the object is in defined angle, if it is, angle color will be red, otherwise green. But my code does not work in some cases...
in this case it work:
in this too:
but here it isn't:
I know that my angle detection code part is not perfect, but I have no idea what I can do.
This is my code:
html:
<html>
<head></head>
<body>
<canvas id="c" width="800" height="480" style="background-color: #DDD"></canvas>
<script src="script.js"></script>
</body>
</html>
js:
window.addEventListener('mousemove', updateMousePos, false);
var canvas = document.getElementById("c");
var context = canvas.getContext("2d");
//mouse coordinates
var mx = 0, my = 0;
draw();
function draw()
{
context.clearRect(0, 0, canvas.width, canvas.height);
//object coordinates
var ox = 350, oy = 260;
context.beginPath();
context.arc(ox,oy,5,0,2*Math.PI);
context.fill();
//circle
var cx = 400, cy = 280;
var r = 100;
var segmentPoints = 20;
var circlePoints = 40;
var spread = Math.PI / 2;
var mouseAngle = Math.atan2(my - cy, mx - cx); //get angle between circle center and mouse position
context.beginPath();
context.strokeStyle = "blue";
context.moveTo(cx + r, cy);
for(var i=0; i<circlePoints; i++)
{
var a = 2 * Math.PI / (circlePoints - 1) * i;
var x = cx + Math.cos(a) * r;
var y = cy + Math.sin(a) * r;
context.lineTo(x, y);
}
context.lineTo(cx + r, cy);
context.stroke();
var objAngle = Math.atan2(oy - cy, ox - cx);
var lowerBorder = mouseAngle - spread / 2;
var biggerBorder = mouseAngle + spread / 2;
/////////////////////////////////////////////ANGLES DETECTION PART
if(objAngle >= lowerBorder && objAngle <= biggerBorder ||
objAngle <= biggerBorder && objAngle >= lowerBorder)
{
context.strokeStyle = "red";
}
else
context.strokeStyle = "green";
context.lineWidth = 3;
//angle center line
context.beginPath();
context.moveTo(cx, cy);
context.lineTo(cx + Math.cos(mouseAngle) * r * 2, cy + Math.sin(mouseAngle) * r * 2);
context.stroke();
//draw spread arc
context.beginPath();
context.moveTo(cx, cy);
for(var i=0; i<segmentPoints; i++)
{
var a = mouseAngle - spread / 2 + spread / (segmentPoints - 1) * i;
var x = cx + Math.cos(a) * r;
var y = cy + Math.sin(a) * r;
context.lineTo(x, y);
}
context.lineTo(cx, cy);
context.stroke();
//show degrees
context.font = "20px Arial";
context.fillText((lowerBorder * 180 / Math.PI).toFixed(2), Math.cos(lowerBorder) * r + cx, Math.sin(lowerBorder) * r + cy);
context.fillText((biggerBorder * 180 / Math.PI).toFixed(2), Math.cos(biggerBorder) * r + cx, Math.sin(biggerBorder) * r + cy);
context.fillText((mouseAngle * 180 / Math.PI).toFixed(2), Math.cos(mouseAngle) * r + cx, Math.sin(mouseAngle) * r + cy);
//update
setTimeout(function() { draw(); }, 10);
}
//getting mouse coordinates
function updateMousePos(evt)
{
var rect = document.getElementById("c").getBoundingClientRect();
mx = evt.clientX - rect.left;
my = evt.clientY - rect.top;
}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
background-color: black;
}
canvas {
position: absolute;
margin: auto;
left: 0;
right: 0;
border: solid 1px white;
border-radius: 10px;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="application/javascript">
// Rotation here is being measured in Radians
// Given two 2D vectors A & B, the angle between them can be drawn from this formula
// A dot B = length(a) * length(b) * cos(angle)
// if the vectors are normalized (the length is 1) the formula becomes
// A dot B = cos(angle)
// angle = acos(a.x * b.x + a.y * b.y)
// So here you are concerned with the direction of the two vectors
// One will be the vector facing outward from the middle of your arc segment
// The other will be a directional vector from the point you want to do collision with to the center
// of the circle
var canvasWidth = 180;
var canvasHeight = 160;
var canvas = null;
var ctx = null;
var bounds = {top: 0.0, left: 0.0};
var circle = {
x: (canvasWidth * 0.5)|0,
y: (canvasHeight * 0.5)|0,
radius: 50.0,
rotation: 0.0, // In Radians
arcSize: 1.0
};
var point = {
x: 0.0,
y: 0.0
};
window.onmousemove = function(e) {
point.x = e.clientX - bounds.left;
point.y = e.clientY - bounds.top;
}
// runs after the page has loaded
window.onload = function() {
canvas = document.getElementById("canvas");
canvas.width = canvasWidth;
canvas.height = canvasHeight;
bounds = canvas.getBoundingClientRect();
ctx = canvas.getContext("2d");
loop();
}
function loop() {
// Update Circle Rotation
circle.rotation = circle.rotation + 0.025;
if (circle.rotation > 2*Math.PI) {
circle.rotation = 0.0;
}
// Vector A (Point Pos -> Circle Pos)
var aX = circle.x - point.x;
var aY = circle.y - point.y;
var aLength = Math.sqrt(aX * aX + aY * aY);
// Vector B (The direction the middle of the arc is facing away from the circle)
var bX = Math.sin(circle.rotation);
var bY =-Math.cos(circle.rotation); // -1 is facing upward, not +1
var bLength = 1.0;
// Normalize vector A
aX = aX / aLength;
aY = aY / aLength;
// Are we inside the arc segment?
var isInsideRadius = aLength < circle.radius;
var isInsideAngle = Math.abs(Math.acos(aX * bX + aY * bY)) < circle.arcSize * 0.5;
var isInsideArc = isInsideRadius && isInsideAngle;
// Clear the screen
ctx.fillStyle = "gray";
ctx.fillRect(0,0,canvasWidth,canvasHeight);
// Draw the arc
ctx.strokeStyle = isInsideArc ? "green" : "black";
ctx.beginPath();
ctx.moveTo(circle.x,circle.y);
ctx.arc(
circle.x,
circle.y,
circle.radius,
circle.rotation - circle.arcSize * 0.5 + Math.PI * 0.5,
circle.rotation + circle.arcSize * 0.5 + Math.PI * 0.5,
false
);
ctx.lineTo(circle.x,circle.y);
ctx.stroke();
// Draw the point
ctx.strokeStyle = "black";
ctx.fillStyle = "darkred";
ctx.beginPath();
ctx.arc(
point.x,
point.y,
5.0,
0.0,
2*Math.PI,
false
);
ctx.fill();
ctx.stroke();
// This is better to use then setTimeout()
// It automatically syncs the loop to 60 fps for you
requestAnimationFrame(loop);
}
</script>
</body>
</html>
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
I now have a customized Shape, and this shape is controlled by one global variable. Thus I assume I just need to change this global variable due to frame.time, erase the old shape, and create the new one.
But however it seems not working. The following is the simplified code.
<script>
var toControlShape;
var myDrawFunction(context) {
// toControlShape will be used here.
}
window.onload = function() {
var stage = new Kinetic.Stage({...});
var layer = new Kinetic.Layer();
var myShape = new Kinetic.Shape({
drawFunc: myDrawFunction,
...
});
layer.add(myShape);
stage.add(layer);
var animation = new Kinetic.Animation(function(frame) {
toControlShape = someFunction(frame.time);
myShape.remove();
myShape = new Kinetic.Shape({
drawFunc: myDrawFunction,
...
});
layer.add(myShape);
}, layer);
animation.start();
};
</script>
The shape displays properly as its initial state. But there is no animation.
I am pretty new to Javascript and HTML5. So there might be a lot of anti-patterns in this code. Pointing out them to me is also appreciated.
The complete code is here on jsFiddle
I think your doing it wrong. From the docs:
"Kinetic.Animation which modifies the shape's position with each
animation frame."
So i think you should stop remove the shape and instead just update the shape. Then it should probably the animation part work for you.
html ---
<!DOCTYPE html>
<html>
<head>
<title>Strath</title>
<meta http-equiv="X-UA-Compatible" content="IE=9" />
<script type="text/javascript" src="kinetic-v4.1.2.min.js"></script>
<script src="fiddle.js"></script>
<style type="text/css">
</style>
</head>
<body>
<div id="PureHTML5"></div>
</body>
</html>
js ---
var segsToSkip = 0;
var triangle;
window.onload = function() {
var stage = new Kinetic.Stage({
container: "PureHTML5",
width: 300,
height: 300
});
var layer = new Kinetic.Layer();
/*
* create a triangle shape by defining a
* drawing function which draws a triangle
*/
var box = new Kinetic.Shape({
drawFunc: myDrawFunction,
stroke: "white",
strokeWidth: 8
});
var bbox= new Kinetic.Rect({
x:10,
y:10,
width:10,
height:10,
fill: 'green'
});
triangle = new Kinetic.Shape({
drawFunc: function(context) {
context.beginPath();
context.moveTo(10, 10);
context.lineTo(20, 80);
context.quadraticCurveTo(30, 10, 26, 17);
context.closePath();
context.stroke();
},
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 4
});
layer.add(bbox);
// add the triangle shape to the layer
layer.add(box);
layer.add(triangle);
// add the layer to the stage
stage.add(layer);
var animation = new Kinetic.Animation(function(frame) {
console.log(frame.time);
var newSegsToSkip = Math.round(frame.time / 200);
triangle.setX(newSegsToSkip);
triangle.setDrawFunc(function(context) {
context.beginPath();
context.moveTo(newSegsToSkip, 10);
context.lineTo(newSegsToSkip+20, 80);
context.quadraticCurveTo(30, 10, 26, 17);
context.closePath();
context.stroke();
});
}, layer);
var animation2 = new Kinetic.Animation(function(frame) {
var newSegsToSkip = Math.round(frame.time / 200);
if (newSegsToSkip == segsToSkip) return;
else {
segsToSkip = newSegsToSkip;
box.remove();
box = new Kinetic.Shape({
drawFunc: myDrawFunction,
stroke: "black",
strokeWidth: 8
});
layer.add(box);
}
}, layer);
animation.start();
animation2.start();
};
var myDrawFunction = function(context) {
var x = 50;
var y = 50;
var width = 200;
var height = 200;
var radius = 20;
var dashedLength = 6;
var segsToDraw = 58;
context.beginPath();
context.drawDashedBox(x, y, width, height, radius, dashedLength, segsToSkip, segsToDraw);
context.closePath();
// context.stroke();
//this.fill(context);
this.stroke(context);
}
var CP = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype;
CP.drawDashedBox = function(x, y, width, height, radius, dashedLength, segsToSkip, segsToDraw) {
// init
var env = getStratEnvForRCBox(x, y, width, height, radius, dashedLength, segsToSkip, segsToDraw);
// 'right'->'upper-right'->'down'->'bottom-right'->'left'->'bottom-left'->'up'->'upper-left'->'right'->...
while (env.segsToDraw > 0) {
console.log('drawing direction: ', env.direction, 'segsToDraw:', env.segsToDraw);
env.putToConsole();
if (env.direction == 'right') {
env = drawDashedLineNew(env, x + width - radius, y + height, dashedLength, this);
env.direction = 'bottom-right';
}
else if (env.direction == 'upper-right') {
env = drawDashedArcNew(env, x + width - radius, y + radius, radius, 0, Math.PI / 2, dashedLength, this);
env.direction = 'left';
}
else if (env.direction == 'down') {
env = drawDashedLineNew(env, x, y + height - radius, dashedLength, this);
env.direction = 'bottom-left';
}
else if (env.direction == 'bottom-right') {
env = drawDashedArcNew(env, x + width - radius, y + height - radius, radius, 3 * Math.PI / 2, 2 * Math.PI, dashedLength, this);
env.direction = 'up';
}
else if (env.direction == 'left') {
env = drawDashedLineNew(env, x + radius, y, dashedLength, this);
env.direction = 'upper-left';
}
else if (env.direction == 'bottom-left') {
env = drawDashedArcNew(env, x + radius, y + height - radius, radius, Math.PI, 3 * Math.PI / 2, dashedLength, this);
env.direction = 'right';
}
else if (env.direction == 'up') {
env = drawDashedLineNew(env, x + width, y + radius, dashedLength, this);
env.direction = 'upper-right';
}
else if (env.direction == 'upper-left') {
env = drawDashedArcNew(env, x + radius, y + radius, radius, Math.PI / 2, Math.PI, dashedLength, this);
env.direction = 'down';
}
}
}
function getStratEnvForRCBox(x, y, width, height, radius, dashLength, segsToSkip, segsToDraw) {
var direction = 'right';
var startX, startY;
if (direction == 'down') {
startX = x; startY = y + radius;
} else if (direction == 'right') {
startX = x + radius; startY = y + height;
} else if (direction == 'up') {
startX = x + width; startY = y + height - radius;
} else if (direction == 'left') {
startX = x + width - radius; startY = y;
}
var env = new Environment(startX, startY, 'gap', 0, direction, segsToSkip, segsToDraw);
return env;
}
function drawDashedLineNew(env, endX, endY, dashedLength, context) {
var dx = (endX - env.x), dy = (endY - env.y);
var angle = Math.atan2(dy, dx);
console.log('drawing line: angle =', angle, ' , ', env.gapOrDash, ' =', env.remainingLengthFromLastDraw);
var fromX = env.x, fromY = env.y;
// deal with remining
// we start loop from a fresh dash
if (env.gapOrDash == 'dash') {
// check if we need to skip
if (env.segsToSkip > 0) {
env.segsToSkip --;
} else {
context.moveTo(env.x, env.y);
context.lineTo(env.x + env.remainingLengthFromLastDraw * Math.cos(angle), env.y + env.remainingLengthFromLastDraw * Math.sin(angle));
// check if we quit
env.segsToDraw --;
if (env.segsToDraw == 0) return env;
}
// a full gap
fromX = env.x + (env.remainingLengthFromLastDraw + dashedLength) * Math.cos(angle);
fromY = env.y + (env.remainingLengthFromLastDraw + dashedLength) * Math.sin(angle);
} else if (env.gapOrDash == 'gap') {
fromX = env.x + env.remainingLengthFromLastDraw * Math.cos(angle);
fromY = env.y + env.remainingLengthFromLastDraw * Math.sin(angle);
}
var length = (endX - fromX) / Math.cos(angle);
if (endX - fromX == 0) length = Math.abs(endY - fromY);
var n = length / dashedLength;
var draw = true;
var x = fromX, y = fromY;
context.moveTo(x, y);
for (var i = 0; i < n; i++) {
x += dashedLength * Math.cos(angle);
y += dashedLength * Math.sin(angle);
if (draw) {
// check if we need to skip
if (env.segsToSkip > 0) {
env.segsToSkip --;
} else {
context.lineTo(x,y);
// check if we quit
env.segsToDraw --;
if (env.segsToDraw == 0) return env;
}
} else context.moveTo(x, y);
draw = !draw;
}
// deal with remaining
if (draw) {
// check if we need to skip
if (env.segsToSkip > 0) {
env.segsToSkip --;
} else
context.lineTo(endX, endY);
}
env.x = endX;
env.y = endY;
draw ? env.gapOrDash = 'dash' : env.gapOrDash = 'gap';
env.remainingLengthFromLastDraw = dashedLength - (endX - x) / Math.cos(angle);
return env;
}
function drawDashedArcNew(env, x, y, radius, startAngle, endAngle, dashedLength, context) {
var points = [];
var n = radius * Math.PI * 2/ dashedLength;
var stepAngle = Math.PI * 2 / n;
// deal with remaining
var angle = Math.asin(env.remainingLengthFromLastDraw / 2 / radius) * 2;
if (env.gapOrDash == 'dash') {
var angle = Math.asin(env.remainingLengthFromLastDraw / 2 / radius) * 2;
points.push({
x : (Math.cos(startAngle) * radius) + x,
y : - (Math.sin(startAngle) * radius) + y,
ex : (Math.cos(startAngle + angle) * radius) + x,
ey : - (Math.sin(startAngle + angle) * radius) + y
});
startAngle += stepAngle + angle;
} else {
startAngle += angle;
}
var draw = true;
while(startAngle + stepAngle <= endAngle) {
if (draw) {
points.push({
x : (Math.cos(startAngle) * radius) + x,
y : - (Math.sin(startAngle) * radius) + y,
ex : (Math.cos(startAngle + stepAngle) * radius) + x,
ey : - (Math.sin(startAngle + stepAngle) * radius) + y
});
}
startAngle += stepAngle;
draw = !draw;
}
// deal with the remaining
var endX = (Math.cos(endAngle) * radius) + x;
var endY = - (Math.sin(endAngle) * radius) + y;
//console.log('drawing arc: end-x:', endX, ',end-y:', endY);
if (draw) {
points.push({
x : (Math.cos(startAngle) * radius) + x,
y : - (Math.sin(startAngle) * radius) + y,
ex : endX,
ey : endY
});
}
env.x = endX;
env.y = endY;
draw ? env.gapOrDash = 'dash' : env.gapOrDash = 'gap';
env.remainingLengthFromLastDraw = dashedLength - radius * Math.sin( (endAngle - startAngle) / 2) * 2;
for(p = 0; p < points.length; p++){
//console.log('draw arc seg: from(', points[p].x, ',', points[p].y, ') to (', points[p].ex, ',', points[p].ey, ')');
// check if we need to skip
if (env.segsToSkip > 0) {
env.segsToSkip --;
} else {
context.moveTo(points[p].x, points[p].y);
context.lineTo(points[p].ex, points[p].ey);
// check if we quit
env.segsToDraw --;
if (env.segsToDraw == 0) return env;
}
}
return env;
}
function Environment(x, y, gapOrDash, remainingLengthFromLastDraw, direction, segsToSkip, segsToDraw) {
this.x = x;
this.y = y;
this.gapOrDash = gapOrDash;
this.remainingLengthFromLastDraw = remainingLengthFromLastDraw;
this.direction = direction;
this.segsToSkip = segsToSkip;
this.segsToDraw = segsToDraw;
}
Environment.prototype.putToConsole = function() {
//console.log('Environment:');
//console.log('x:', this.x, ',y:', this.y, 'direction:', this.direction);
//console.log('toSkip:', this.segsToSkip, 'toDraw:', this.segsToDraw);
}