Kinetic-js: How to cache a complete layer as an image - performance

I have a problem an hope to find any solution for it.
I am using Kinetic.js to create a HMI solution with special look-and-feel. Therefor I have created a function that creates 3 layers for a stage: a background layer with a grid, a layer with static shapes for the base layout of the HMI-screen and the third layer for all interactive elements (like buttons, valuedisplays and so on...). Now I want to cache the grid and the static layer to improve performance, because this layers will never change until the whole HMI-screen will change...
As a test I started to code the caching for the grid layer using the following code:
// Create a grid layer if showGrid is set to TRUE...
console.log('Start to create background grid if required');
if (this.actualPageConfig.showGrid) {
var grid = new Kinetic.Layer();
for (var x=1; x <= this.cols; x++) {
var eLoc = LCARSElements.posToRealPos(x, 1, this.cols, this.rows);
if (x <= this.actualPageConfig.columns) {
grid.add(new Kinetic.Line({
points: [eLoc.x, eLoc.y, eLoc.x, eLoc.height],
stroke: "red",
strokeWidth: 1,
lineCap: "round",
lineJoin: "round"
}));
}
}
for (var y=1; y <= this.rows; y++) {
var eLoc = LCARSElements.posToRealPos(1, y, this.cols, this.rows);
if (y <= this.actualPageConfig.rows) {
grid.add(new Kinetic.Line({
points: [eLoc.x, eLoc.y, eLoc.width, eLoc.y],
stroke: "red",
strokeWidth: 1,
lineCap: "round",
lineJoin: "round"
}));
}
}
// Add grid layer to stage
//this.stage.add(grid); <-- to be replaced by cache image
// Convert grid into an image and add this to the stage
console.log('convert grid to image to increase performance');
grid.toImage({
width: displayManager.stage.getWidth(),
height: displayManager.stage.getHeight(),
callback: function(img) {
var cacheGrid = new Kinetic.Image({
image: img,
x: 0,
y: 0,
width: displayManager.stage.getWidth(),
height: displayManager.stage.getHeight()
});
console.log('insert grid-image to stage');
displayManager.stage.add(cacheGrid);
console.log('redraw stage...');
displayManager.stage.draw();
}
});
}
My problem is, that's not working. The grid is not visible any more and the console log shows the following error information:
Type error: layer.canvas is undefined
layer.canvas.setSize(this.attrs.width, this.attrs.height); kinetic.js (Zeile 3167)
As I already figured out the error rise when the code "displayManger.stage.add(cacheGrid) will be executed (displayManager is the outside-class where this code snipped reside).
Can anyone see where I made the mistake? When I directly add the layer grid anything works fine...
I have created a jsfiddle to demonstrate the problem: jsfiddle
In fiddle you can run both versions by changing one parameter. Hope this helps....
Thanks for help.
Best regards
Thorsten

Actually, the problem is simpler than you might think - after caching the layer into an image, you're trying to add an image object directly to the stage (you can't do that).
To fix the problem, you need to create a new layer, say cahcedLayer, add the image to cachedLayer, and then add cachedLayer to the stage.
Check out the KineticJS info page to learn more about Node nesting:
https://github.com/ericdrowell/KineticJS/wiki

http://rvillani.com/testes/layer-to-image/
I've made this test and it worked. First, I draw 1000 squares to a layer, add this layer to a hidden stage then make an image from this stage using stage.toDataURL(). When the callback returns, I just create an Image from the data and a Kinetic.Image from the Image. Then I add it to a layer on my main (visible) stage.
Code (be sure to have a div called 'invisible'):
window.onload = function()
{
var stage = new Kinetic.Stage({
width: 520,
height: 480,
container: 'container'
});
var outerStage = new Kinetic.Stage({
width: stage.getWidth(),
height: stage.getHeight(),
container: 'invisible'
});
var layerToCache = new Kinetic.Layer();
var layer = new Kinetic.Layer();
var group = new Kinetic.Group({offset: [stage.getWidth(), stage.getHeight()]});
var anim = new Kinetic.Animation(function(frame){
group.rotate(0.02);
}, layer);
var fills = ['red', 'green', 'blue', 'orange', 'purple', 'cyan',
'black', 'brown', 'forestgreen', 'gray', 'pink'];
for (var i = 0; i < 10000; ++i)
{
(function ()
{
var size = Math.random() * 60 + 20;
var square = new Kinetic.Rect({
width: size,
height: size,
fill: fills[i % fills.length],
x: Math.random() * stage.getWidth() - 20,
y: Math.random() * stage.getHeight() - 20
});
layerToCache.add(square);
})();
}
var squaresImg = new Kinetic.Image();
outerStage.add(layerToCache);
outerStage.toDataURL({
callback: function (dataURL){
outerStage.clear();
var img = new Image();
img.src = dataURL;
img.onload = function () {
squaresImg.setImage(img);
squaresImg.setX(squaresImg.getWidth() >> 1);
squaresImg.setY(squaresImg.getHeight() >> 1);
group.setX(stage.getWidth() >> 1);
group.setY(stage.getHeight() >> 1);
group.add(squaresImg);
layer.add(group);
layer.draw();
anim.start();
}
}
});
var div = document.getElementById('invisible');
div.parentNode.removeChild(div);
stage.add(layer);
}

Related

How to use Konva to apply the matrix obtained by getTransform to a larger canvas proportionally

Our project is an online DIY album. Users fill in photos in the frame. Konva is required to achieve a mask function similar to Photoshop, similar to the effect of photos pressed under the frame. There may be many picture frames in my canvas, and only one canvas can not achieve this function. Therefore, I need to use a new canvas to draw this, and then draw the contents of this mask canvas onto Konva's canvas.
Based on this idea, I realized the basic needs. Our requirement is that users can drag, rotate or scale photos. Therefore, I drew a transparent element on Konva's main canvas (Canvas 1) with the same size and position as the photo in the mask canvas (Canvas 2), and then bound it with transformer.With this transformer, I can change the position, size and rotation angle of the transparent elements, and then transfer the matrix of the transparent elements to the photos in the mask canvas through getTransform and setTransform, so that the photos in the mask can rotate, scale or translate with the transparent elements.
When the width and height of the mask canvas (Canvas 2) are the same as those of the elements finally drawn on the main canvas (Canvas 1), everything is OK. However, in order to ensure a clearer final effect, we set the width and height of the mask canvas (Canvas 2) to a higher value. Then there is a problem. When I drag or rotate the transparent elements, the position of the photos in the mask does not completely follow the transparent elements, but there is a deviation.
I think this is because the size of the transparent element is different from the size of the photo in the mask canvas (Canvas 2), so it is not possible to directly set the matrix obtained by getTransform to the photo. In this process, a transformation is required. It should be OK to set the transformed matrix to the photo in the mask. The problem is that I don't know how to carry out such a transformation.
Here's the code:
//canvas 1
let container = document.getElementById("container");
let stage = new Konva.Stage({
container: container,
width: 800,
height: 800
});
layer = new Konva.Layer();
stage.add(layer);
let group = new Konva.Group({
x: 100,
y: 100,
width: 200,
height: 200,
clipX: 0,
clipY: 0,
clipWidth: 200,
clipHeight: 200
});
let shape = await drawMaskPhoto();
group.add(shape);
let transparentRect = new Konva.Rect({
width: 200,
height: 200,
x:0,
y:0,
type:"transparentRect"
});
group.add(transparentRect);
transparentRect.on('transform',async function (e) {
let transform = e.target.getTransform().m;
shape = await drawMaskPhoto(transform);
});
function newImg(src) {
return new Promise((resolve, reject) => {
let img = new Image();
img.src = src;
img.onload = () => {
resolve(img)
}
})
}
async function drawMaskPhoto(transform){
//canvas 2
let maskCanvas = document.createElement("canvas");
let mask = await newImg(mask.imgPath);
let photo = await newImg(photo.imgPath);
//To ensure clarity, I set the new canvas size to be 5 times the size of the elements finally drawn on the main canvas (1000*1000)
maskCanvas.width = 1000;
maskCanvas.height = 1000;
let ctx = maskCanvas.getContext("2d");
ctx.drawImage(mask, 0, 0, 1000, 1000);
ctx.globalCompositeOperation = 'source-atop';
if(transform){
ctx.setTransform(...transform);
}
ctx.drawImage(photo, 0, 0, 1000, 1000);
let shape = new Konva.Image({
image:maskCanvas,
width:200,
height:200
})
return shape
}
The effect picture is like this
The effect picture drawn
Effect picture after rotation
let traceEle = $('#trace');
/*
* SyncHandler is a DIY object for synchronising canvases. In this case we have one leader and one follower.
* An object is used to scope the code. Create new instaces via
* let myStage = new Konva.Stage(..all usual Konva stage stuff...)
* let myHandler = new syncHandler(myStage, 'h4'); // where h4 = a useful trace prefix so we can see which handler is fired.
*/
function syncHandler(stage, handlerName){
let followers = [], // In this demo we have only one listening canvas but we will assume there could be many.
shapes = {}; // Each shape created is placed in this object thus forming a quick lookup list based on id.
// We need all shapes to have a unique ID so we use this func to make them.
// These are not entirely compliant GUID's but meet our needs.
function getGuid(){
let fC=function () {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1).toUpperCase();
}
return (fC() + fC() + "-" + fC() + "-" + fC() + "-" + fC() + "-" + fC() + fC() + fC());
}
// Fires during init stage - set the stage id if not set already.
stage.id(getGuid());
// Add a follower to the list of followers.
this.addFollower = function(newFollower){
followers.push(newFollower);
}
// Func to make a proxy for the shape's attrs arry.
function makeProxy(target){
let prxy = new Proxy(target, {
// this code fires when the shape has a change to its attrs array. It allows us
// to intervene with the prop change, send the change to the listerners
// target is the shape's attrs array which will shortly receive the new value.
set(array, prop, value, target) {
changeAttr(prop, value, target); // Invoke the function to communicate the change to any follower[]
// finally let the target - the shape's attr array get the change.
return Reflect.set(array, prop, value, target);
},
deleteProperty(array, prop, target) {
// Included in case needed in future.
let msg = {type: 'attrdel', id: target["id"], name: prop};
sendMessage(msg);
// finally let the target - the shape's attr array get the change.
return Reflect.deleteProperty(array, prop);
}
})
return prxy;
}
// This func is a wrapper for the 'new Konver.<ShapeName>()' process. It is required to:
// - ensure that the new shape will have an id;
// - hide the wiring up of the shape.attrs prox;
// - add the shape into our shapes list, keyed on the assigned id;
// - send the message about the new object to the follower[] canvases.
this.makeShape = function(typeName, attrs){
let shape = null;
attrs = (typeof attrs == 'undefined' ? {} : attrs); // if attrs is not supplied then make an empty array
attrs.id = (typeof attrs.id === 'undefined' ? getGuid() : attrs.id); // ensure there is an ID.
shape = new Konva[typeName](); // Make the actual Konva shape in this canvas.
shape.setAttrs(attrs); // Set the attrs for the new shape.
shape.attrs = makeProxy(shape.attrs);
if (typeof shapes[attrs.id] === 'undefined'){
shapes[attrs.id] = shape;
}
// Send the message about the new shape to any follower[]
let msg = {type: 'makeShape', name: typeName, attrs: attrs };
sendMessage(msg);
return shape; // Hand back the shape to the code that created it.
}
// This func is a wrapper for Konva.container.add() or move(). It takes
// as args the shape being moved and the container that is to be the new parent.
this.changeParent = function(shapeObj, newParentObj){
newParentObj.add(shapeObj); // add shape to new parent.
// Send the message about the new shape to any follower[]
let msg = {type: 'changeParent', id: shapeObj.id(), parentId: newParentObj.id(), parentType: newParentObj.getType()};
sendMessage(msg);
}
/* this func is a wrapper for the Konva.shape.setAttr() method.
* Network comms are costly - we do not want to send messages about propos that either have not changed or
* where the change to a numeric property is insignificant.
*/
function changeAttr(prop, value, target){
let currentVal = target[prop],
sendIt = true; // flag to indicate change is to be sent - overridden below for numeric types if needed.
if (currentVal !== value){
if ( typeof(value) === "number" ){
if (Math.abs(currentVal - value) < 0.01){ // adjust or remove this tollerence as needed.
sendIt = false;
}
}
if (sendIt){
// make the message
let msg = {type: 'changeAttr', id: target["id"], name: prop, value: value };
sendMessage(msg);
}
}
return true;
}
// Func to convert the given message to a JSON string and send it to any followers.
function sendMessage(msg){
if (followers.length === 0){ // no send if no listening followers.
return;
}
let jsonMsg = JSON.stringify(msg);
for (let i = 0; i < followers.length; i++){
followers[i].processMessage(jsonMsg);
}
}
/* In this func we process a received change message. In this demo this is simply one object calling a func in another
* but in the final version this will be talking via peer-to-peer between browsers. We receive a message in JSON format
* containing the change information. The 'type' value gives either 'makeShape', 'changeParent', or 'changeAttr'.
*
* Note that when this runs it is within the context of a 'following' syncHandler instance, not the sending instance!
*
*/
this.processMessage = function(changeInfo){
let change = JSON.parse(changeInfo), // parse the JSON into a JS object. Note you will want a try-catch here !
shape = null;
switch (change.type){
case "makeShape": // a new shape message.
shape = this.makeShape(change.name, change.attrs); // make the shape in the follower syncHandler - this works
// in this demo because the follwower has no followers of its own.
// If it _did_ have followers a deadlock would occur !
trace(handlerName + ".makeShape: " + change.name + ' ' + shape.id()); // record a trace of what is going on
shapes[shape.id()] = shape; // note the shape in our shape list.
break;
case "changeParent": // an existing shape is changing parent container - like from layer A to layer B.
trace(handlerName + '.changeParent: id=' + change["id"])
shape = shapes[change.id]; // get the Konva shape instance that is moving parent
// Special case for adding to stage
if (change.parentType === "Stage"){
stage.add(shape)
}
else {
let parentContainer = shapes[change.parentId]; // get the Kona shape that is to be the new container.
parentContainer.add(shape); // execute the Konva command to switch parents.
}
break;
case "changeAttr": // an attribute of a shape has changed - mirror the change in this follower.
trace(handlerName + '.changeAttr: id=' + change["id"] + ' - ' + change.name + ' = ' + change.value);
shape = shapes[change.id];
shape.setAttr(change.name, change.value);
break;
}
}
} // end of the syncHandlerobject declaration.
// a simple trace output function so we can see some of what is happening - better than console.log!
function trace(msg){
traceEle.prepend('<pre>' + msg + ' </pre>');
}
/* from here onwards is Konva canvas admin */
// Making the stage is standard Konva API code.
let stage1 = new Konva.Stage({container: "container1", width: $('#container1').width(), height: $('#container1').height()});
// And now we create the handler object.
let handler1 = new syncHandler(stage1, 'sh1');
// Making the stage is standard Konva API code.
let stage2 = new Konva.Stage({container: "container2", width: $('#container2').width(), height: $('#container2').height()});
let handler2 = new syncHandler(stage2, 'sh2');
// Very importantly - we inform handler1 than handler2 is listening and wants to know about changes.
handler1.addFollower(handler2);
// The stage object was made via standard Konva API but for all other containers and shapes we use the handler
// function which adds id and wires up listener on attrs list.
let layer1 = handler1.makeShape("Layer");
// Add the layer to the stage.
// Adding a shape is done via syncHandler so that we can capture and broadcast the change
handler1.changeParent(layer1, stage1);
// Make a rect.
let rect1 = handler1.makeShape("Rect", {x: 20, y: 10, width: 100, height: 80, fill: 'cyan', draggable: true});
// Add rect1 to layer1 via syncHandler
handler1.changeParent(rect1, layer1);
// Make a circle
let circle1 = handler1.makeShape("Circle", {x: 140, y: 100, radius: 40, fill: 'magenta'});
// Add circle1 to layer1 via syncHandler
handler1.changeParent(circle1, layer1);
// Make a pentagon
let radialGradPentagon1 = handler1.makeShape("RegularPolygon", {
x: 500,
y: stage1.height() / 2,
sides: 5,
radius: 70,
fillRadialGradientStartPoint: { x: 0, y: 0 },
fillRadialGradientStartRadius: 0,
fillRadialGradientEndPoint: { x: 0, y: 0 },
fillRadialGradientEndRadius: 70,
fillRadialGradientColorStops: [0, 'red', 0.5, 'yellow', 1, 'blue'],
stroke: 'black',
strokeWidth: 4,
draggable: true,
});
// Add radialGradPentagon1 to layer1 via syncHandler
handler1.changeParent(radialGradPentagon1, layer1);
//
// Now we carry out a handful of attribute changes on the shapes to confirm it works !
//
// Move the rect to x = 101
rect1.x(101)
// Fill rect with red and rotate 45 degrees.
rect1
.fill('red')
.rotation(45);
rect1.on('mousedown', function(e){
e.cancelBubble = true;
})
// make circle draggable
circle1.draggable(true);
// Change the pentagon gradient
radialGradPentagon1
.fillRadialGradientEndRadius(60)
.fillRadialGradientColorStops([0, 'red', 0.5, 'yellow', 1, 'blue']);
// We will also now make a transformer on Stage 1 to experiment with dynamic attr changes.
var tr = handler1.makeShape("Transformer", {
anchorStroke: 'red',
anchorFill: 'yellow',
anchorSize: 20,
borderStroke: 'green',
borderDash: [3, 3],
nodes: [],
});
handler1.changeParent(tr, layer1);
// attach the transformer to the rect
tr.nodes([rect1])
layer1.on('mousedown', function(){
traceEle.html('');
})
// add a new rect to be used as a mouse-selection rectangle via click & drag on stage1.
var selectionRectangle = handler1.makeShape("Rect", {
name: 'selectionRect',
fill: 'rgba(0,0,255,0.5)',
visible: false,
});
// Following copied from https://konvajs.org/docs/select_and_transform/Basic_demo.html
// Add selectionRectangle to layer1 via syncHandler
handler1.changeParent(selectionRectangle, layer1);
let x1, y1, x2, y2;
stage1.on('mousedown touchstart', (e) => {
// do nothing if we mousedown on any shape
if (e.target !== stage1) {
return;
}
x1 = stage1.getPointerPosition().x;
y1 = stage1.getPointerPosition().y;
x2 = stage1.getPointerPosition().x;
y2 = stage1.getPointerPosition().y;
selectionRectangle.visible(true);
selectionRectangle.width(0);
selectionRectangle.height(0);
});
stage1.on('mousemove touchmove', () => {
// do nothing if we didn't start selection
if (!selectionRectangle.visible() ) {
return;
}
x2 = stage1.getPointerPosition().x;
y2 = stage1.getPointerPosition().y;
selectionRectangle.setAttrs({
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1),
});
});
stage1.on('mouseup touchend', () => {
// no nothing if we didn't start selection
if (!selectionRectangle.visible()) {
return;
}
// update visibility in timeout, so we can check it in click event
setTimeout(() => {
selectionRectangle.visible(false);
});
var shapes = stage1.find();
var shapes = layer1.getChildren(function(node){
return node.name() !== 'selectionRect' && node.getClassName() != "Transformer";
});
var box = selectionRectangle.getClientRect();
var selected = shapes.filter((shape) =>
Konva.Util.haveIntersection(box, shape.getClientRect())
);
tr.nodes(selected);
});
.container {
width: 800px;
height: 300px;
background-color: silver;
margin: 5px;
}
#trace {
max-height: 200px;
overflow-y: scroll;
font-family: 'Courier'
}
pre {
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<script src="https://unpkg.com/konva#8/konva.min.js"></script>
<div id='container1' class='container'></div>
<div id='container2' class='container'></div>
<p id='trace'></p>

Drawing a spreadsheet like grid in canvas with konva

I am working on a Gantt-like task display using HTML5 canvas, and the Konvajs canvas library.
Deconstructing a Gantt chart into its components leads me currently to a view as below. Here 1 is the list of tasks, 2 is the task bar area, 3 is a task bar, and 4 is a text cell.
For this question I am seeking code to construct 1 & 4. The data to be displayed will be delivered in plain JS objects with a nested list of tasks where each task has a number, name, assigned-to person name, start date, end date, days duration, and % complete.
So the requirement is to be able to construct a spreadsheet-like panel such as is seen on the left hand side of a Gantt chart.
I have something part developed which I shall post as an answer. However this seems like such as common need that I am hoping there is someone out there with code they can cut & paste into SO to lead the way.
Note: Gantt in sample image is from Vertex42.
So here is my own fumbling attempt at an approach. Can anyone improve upon it or am I going down the wrong road.
EDIT: I now have a fledgling component for drawing text into the spreadsheet-like cells, including the percent complete shading. To keep this answer uncluttered, this additional component is in this codepen.
// this is the object that defines our grid
//
var gridData = { name: 'grid1', width: 350, height: 400, rowHeight: 24, padding: 4, fill: 'azure', gridLineColor: '#ccc', header: {size: 16, fill: 'black', color: 'white' }, data: {size: 16, fill: 'azure', color: 'black' },
row: [
{ cells: // row 1
[
{ width: 50, text: 'Item', style: 'header'},
{ width: 240, text: 'Name', style: 'header'},
{ width: 60, text: 'Status', style: 'header'},
]
},
{ cells: // row 2
[
{ text: '1'},
{ text: 'Find tea pot'},
{ text: '100%'},
]
},
{ cells: // row 3
[
{ text: '2'},
{ text: 'Boil water'},
{ text: '60%'},
]
}
]
}
// From here on could be wrapped into a component that churns out grids. Maybe you pass in the id of the stage container
// and the data model you want to produce, etc.
// Set up the canvas / stage
var stage = new Konva.Stage({container: 'container1', width: 600, height: 300});
// Add a layer
var layer = new Konva.Layer({draggable: false});
stage.add(layer);
// make a main group for the grid, call it a panel. Assigning a name may be handy later
var panel = new Konva.Group({name: gridData.name});
layer.add(panel); // Add the panel to the layer
// a group has no visual properties. Add a background rect to hold the colour fill
var panelRect = new Konva.Rect({width: gridData.width, height: gridData.height, fill: gridData.fill})
panel.add(panelRect);
var topLeft = {x: 0, y: 0}; // Since we are drawing a grid, we need to compute the position of each cell
for (var i = 0; i < gridData.row.length; i = i + 1){
topLeft.x = 0; // reset x at start of each row
// iterate for each cell on the row
for (var j = 0; j < gridData.row[i].cells.length; j = j + 1){
var cell = new Konva.Rect({name: 'cellBg', // assign a name for later searching
x: topLeft.x, y: topLeft.y, // position as computed
width: gridData.row[0].cells[j].width, // use the first row from celldate to get the cell width
height: gridData.rowHeight, // grid has a uniform row height
stroke: gridData.gridLineColor, // and line colour
strokeWidth: 1, // use a set line width but you can add to the gridData object as needed.
fill: (i === 0 ? gridData.header.fill : gridData.data.fill), // use the given header text color
});
panel.add(cell);
// Add text to the cell. Note that if you wanted to be using alignments you would need to draw the text off-screen and
// get width/height of that text then use those values for positioning calculations. Once you have the rect size of the
// text, all the alignments are simple math.
var text = new Konva.Text({ x: topLeft.x + gridData.padding, // add padding to locate the text nicely into the cell
y: topLeft.y + gridData.padding,
// use the given text size
fontSize: (i === 0 ? gridData.header.size : gridData.data.size),
// use the given header text color
fill: (i === 0 ? gridData.header.color : gridData.data.color),
text: gridData.row[i].cells[j].text, // set the text value.
listening: false // stop text interfering with mouse events
});
panel.add(text);
cell.on('mouseover', function(evt){
var shape = evt.target;
$(shape).data('bgColor', shape.fill());
shape.fill('lime');
layer.draw();
})
cell.on('mouseout', function(evt){
var shape = evt.target;
shape.fill($(shape).data('bgColor'));
layer.draw();
})
topLeft.x = topLeft.x + gridData.row[0].cells[j].width; // offset the computed next cell x value by the width of the cell
}
topLeft.y = topLeft.y + gridData.rowHeight; // offset the computed next cell y value by the height of the row
}
layer.draw();
stage.draw();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/2.5.1/konva.min.js"></script>
<div id='container1' style="width: 300px, height: 200px; background-color: silver;"></div>

Coin spin effect animation on grouped shapes

I have been asked, using Konvajs, to work out an animation that will rotate a circle as if spinning on its central x-axis. So imagine a coin spinning on a table. The intention is to reveal some text on the circle. At the start the circle is fully visible as if from behind so no text visible, then it flips to reveal the text.
I have this code that does a rotation like a spinning wheel.
Can anyone give me a tween / animation approach that would achieve the spinning coin effect?
// the tween has to be created after the node has been added to the layer
var tween = new Konva.Tween({
node: group,
duration: 4,
rotation: 360,
easing: Konva.Easings.BackEaseOut
}
});
tween.play();
After some research it looks like a 3D spin requires heavier lifting which may not be available or work well on mobile.
A good second-best appears to be using scaleX and animating from 0 > 1.
group.scaleX(0);
var tween = new Konva.Tween({
node: group,
duration: .25,
scaleX: 1,
easing: Konva.Easings.EaseOut
});
Here is an example of the second-best version using scaleX() effect. Because of the need to calculate scaleX() and control visibility of the text so as to make it appear that the disc is solid, I moved away from a tween and over to an animation().
// Set up the canvas / stage
var s1 = new Konva.Stage({container: 'container1', width: 300, height: 200});
// Add a layer for line
var layer = new Konva.Layer({draggable: false});
s1.add(layer);
// just a plain JS object to keep common variables in hand.
var cfg = { w: 300, h: 200, r: 80, txtSize: 520};
var group = new Konva.Group();
var circle = new Konva.Circle({x: cfg.w/2, y: cfg.h/2, radius: cfg.r, fill: 'DodgerBlue', stroke: 'DeepPink', strokeWidth: 5})
group.add(circle)
var textValue = new Konva.Text({
id: "t1",
x: cfg.w/2,
y: cfg.h/2,
text: '',
fill: 'DeepPink ',
fontSize: cfg.txtSize
});
group.add(textValue);
textValue.offset({x: textValue.getWidth()/2, y: textValue.getHeight()/2});
layer.add(group)
// to spin a group about a point, set the offset to that point, then set the x & y to that point to !
var pos = group.getClientRect();
RotatePoint(group, {x: pos.x + pos.width/2, y: pos.y + pos.height/2});
// Everything is ready so draw the canvas objects set up so far.
s1.draw()
$('#st').on('click', function(){
group.scaleX(1);
var txt = $('#theText').val();
setValue(txt);
})
// set the offset for rotation to the given location and re-position the shape
function RotatePoint(shape, pos){ // where pos = {x: xpos, y: yPos}
var initialPos = shape.getAbsolutePosition();
var moveBy = {x: pos.x - initialPos.x, y: pos.y - initialPos.y};
// offset is relative to initial x,y of shape, so deduct x,y.
shape.offsetX(moveBy.x);
shape.offsetY(moveBy.y);
shape.x(initialPos.x + moveBy.x);
shape.y(initialPos.y + moveBy.y);
}
var setValue = function(newText){
// work out scaling to make text fit into the circle
var txt = this.layer.find('#t1')[0];
txt.text(newText).scale({x:1, y: 1})
var txtSize = txt.getClientRect();
var maxW = (cfg.r); // max allowed width of text
var txtScaleW = (txtSize.width > maxW ? ( maxW / txtSize.width) : 1);
var maxH = cfg.r; // max allowed height of text
var txtScaleH = (txtSize.height > maxH ? ( maxH / txtSize.height) : 1);
// finally decide which is the worst case and use that scaling
var txtScale = ( txtScaleW > txtScaleH ? txtScaleH : txtScaleW);
txt.scale({x: txtScale, y: txtScale});
txt.offset({x: txt.getWidth()/2, y: txt.getHeight()/2});
layer.draw()
}
// set initial text & spin !
setValue('BBB');
var anim, pos = 0, frameCnt = 0
if (anim) {anim.stop(); }
anim = new Konva.Animation(function(frame) {
frameCnt = frameCnt + 1;
if (frameCnt % 2 === 0){
pos = pos + .2
var scaleX = Math.sin(pos)
textValue.visible(scaleX < 0 ? false : true);
group.scaleX(scaleX);
if (pos % 360 === 0){ console.log('spin') }
}
}, layer);
anim.start();
div
{
float: left;
margin: 0 5px;
}
p
{
margin: 0 5px 5px 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/2.5.1/konva.min.js"></script>
<div id='container1' style="width: 300px, height: 200px;"></div>
<div>
<p> <input type='text' id='theText' value='BBB' /> <button id='st'>Change text</button> </p>
</div>

How to add Text-transform on text in canvas using kinetic js?

I want to know how could i convert text into uppercase in canvas using kinetic js.
Here is my code for canvas with image and text:
top_text_val = "INSERT TOP TEXT"; //Default top text on meme
bottom_text_val = "INSERT BOTTOM TEXT"; //Default bottom text on meme
var stage = new Kinetic.Stage({
container: 'meme-img',
width: 735, //width of container
height: 540, //height of container
});
var layer = new Kinetic.Layer(); //new object of kinetic layer
var imageObj = new Image(); //new image object to load image
var top_text;
var bottom_text;
//image onload event code
imageObj.onload = function() {
var image_shape = new Kinetic.Image({ //set image display dimensions
image: imageObj,
width: 735,
height: 540
});
top_text = new Kinetic.Text({ // Code to display top default text on meme image
x: 100,
y: 35,
text: top_text_val,
fontSize: 80,
fontFamily: 'Impact',
fill: '#fff',
align: "center",
stroke: 'black',
strokeWidth: 4,
});
layer.add(image_shape); // add the shape to the layer
layer.add(top_text); // add top text to the layer
stage.add(layer); // add the layer to the stage
};
if(image_link.trim().length>0) imageObj.src = image_link;
Now i want to set text and want to set text in Uppercase on keyup event of some text box. Please refer below code for same:
$('#txtTop').on('keyup',function(){
var value = $(this).val();
top_text.setText(value);
if($('#chkAllCaps').is(':checked')){
//here i need code to set uppercase text-transform
}
layer.draw();
});
I have tried text-transform also but it is not working.
Please kindly help me out.
Thanks!!
string.toUpperCase() would work in this case. The toUpperCase() method converts a string to uppercase letters and it would be better to add mouse click event on #chkAllCaps element and make a function for render.
This is jsfiddle: http://jsfiddle.net/6t7ohbnh/1/
Javascript
$('#txtTop').on('keyup', function() {
render();
});
$('#chkAllCaps').on('click', function() {
render();
});
function render(){
var value = $('#txtTop').val();
if ($('#chkAllCaps').is(':checked')) {
top_text.setText(value.toUpperCase());
}else{
top_text.setText(value);
}
layer.draw();
}

complex clipping boundary in kinetic.js with draggable image

Check out my html5 based clipping constraint on
http://shedlimited.debrucellc.com/test3/canvaskinclip.html
(messing with jsfiddle on http://jsfiddle.net/aqaP7/4/)
So, in html5 I can easily draw a shaped boundary like the following:
context.beginPath();
context.moveTo(5, 5);
context.lineTo(34, 202);
context.lineTo(2, 405);
context.lineTo(212, 385);
context.lineTo(425, 405);
context.lineTo(400, 202);
context.lineTo(415, 10);
context.lineTo(212, 25);
context.clip();
In kinetic.js though, all I see for clipping options is: height, width, and x, y,
I came across the following : Mask/Clip an Image using a Polygon in KineticJS, but the inner/fill image can't be set to draggable
any help please!
In the new kineticJS versions, a lot of the work is done in the background for you.
Take a look at this tutorial:
This fiddle gets you pretty close, here's the code:
<body>
<div id="container"></div>
<script src="http://www.html5canvastutorials.com/libraries/kinetic-v4.3.0-beta2.js"></script>
<script>
function loadImages(sources, callback) {
var images = {};
var loadedImages = 0;
var numImages = 0;
// get num of sources
for(var src in sources) {
numImages++;
}
for(var src in sources) {
images[src] = new Image();
images[src].onload = function() {
if(++loadedImages >= numImages) {
callback(images);
}
};
images[src].src = sources[src];
}
}
function draw(images) {
var stage = new Kinetic.Stage({
container: 'container',
width: 600,
height: 700
});
var layer = new Kinetic.Layer();
var patternPentagon = new Kinetic.RegularPolygon({
x: 220,
y: stage.getHeight() / 4,
sides: 5,
radius: 70,
fillPatternImage: images.yoda,
fillPatternOffset: [-220, 70],
stroke: 'black',
strokeWidth: 4,
draggable: true
});
patternPentagon.on('dragmove', function() {
//this.setFillPatternImage(images.yoda);
//this.setFillPatternOffset(-100, 70);
var userPos = stage.getUserPosition();
this.setFillPatternOffset(-userPos.x,-userPos.y);
layer.draw();
this.setX(220);
this.setY(stage.getHeight() / 4);
});
layer.add(patternPentagon);
stage.add(layer);
}
var sources = {
darthVader: 'http://www.html5canvastutorials.com/demos/assets/darth-vader.jpg',
yoda: 'http://www.html5canvastutorials.com/demos/assets/yoda.jpg'
};
loadImages(sources, function(images) {
draw(images);
});
</script>
</body>
There is a more complex/accurate way of doing this without making it a background pattern, like with grouping objects together

Resources