I have an animated component where you can select one of seventeen circles. It looks like this so far:
I would like to add an animation that scales the circle as it gets closer to the center. How do I do that?
Until now I tried to calculate the x value of the circle as Math.sin(index*deltaTheta*Math.PI/180 + Math.PI)*Radius and use this value in a functions which maps to a scaling factor (e.g. a gaussian). This fails because the x value does not change, because I am using CSS transform rotate.
Then I tried to use a different interpolating range for every single circle, but did not achieved a satisfying result.
My code:
import React, { Component } from 'react'
import { Text, View, PanResponder, Animated, Dimensions } from 'react-native'
import styled from 'styled-components'
import Circle from './Circle'
const SCREEN_WIDTH = Dimensions.get('window').width
const Container = styled(Animated.View)`
margin: auto;
width: 200px;
height: 200px;
position: relative;
top: 100px;
`
const gaussFunc = (x, sigma, mu) => {
return 1/sigma/Math.sqrt(2.0*Math.PI)*Math.exp(-1.0/2.0*Math.pow((x-mu)/sigma,2))
}
const myGaussFunc = (x) => gaussFunc(x, 1/2/Math.sqrt(2*Math.PI), 0)
const circles = [{
color: 'red'
}, {
color: 'blue'
}, {
color: 'green'
}, {
color: 'yellow'
}, {
color: 'purple'
}, {
color: 'black'
}, {
color: 'gray'
}, {
color: 'pink'
}, {
color: 'lime'
}, {
color: 'darkgreen'
}, {
color: 'crimson'
}, {
color: 'orange'
}, {
color: 'cyan'
}, {
color: 'navy'
}, {
color: 'indigo'
}, {
color: 'brown'
}, {
color: 'peru'
}
]
function withFunction(callback) {
let inputRange = [], outputRange = [], steps = 50;
/// input range 0-1
for (let i=0; i<=steps; ++i) {
let key = i/steps;
inputRange.push(key);
outputRange.push(callback(key));
}
return { inputRange, outputRange };
}
export default class SDGCircle extends Component {
state = {
deltaTheta: 360/circles.length,
Radius: 0, // radius of center circle (contaienr)
radius: 25, // radius of orbiting circles
container: { height: 0, width: 0 },
deltaAnim: new Animated.Value(0),
}
offset = () => parseInt(this.state.container.width/2)-this.state.radius
_panResponder = PanResponder.create({
nMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (event, gestureState) => true,
onPanResponderGrant: () => {
const { deltaAnim } = this.state
deltaAnim.setOffset(deltaAnim._value)
deltaAnim.setValue(0)
},
onPanResponderMove: (event, gestureState) => {
const { deltaAnim, scaleAnim, deltaTheta, Radius } = this.state
deltaAnim.setValue(gestureState.dx)
console.log(deltaAnim)
},
onPanResponderRelease: (event, gestureState) => {
const {dx, vx} = gestureState
const {deltaAnim} = this.state
deltaAnim.flattenOffset()
Animated.spring(deltaAnim, {
toValue: this.getIthCircleValue(dx, deltaAnim),
friction: 5,
tension: 10,
}).start(() => this.simplifyOffset(deltaAnim._value));
}
})
getIthCircleValue = (dx, deltaAnim) => {
const selectedCircle = Math.round(deltaAnim._value/(600/circles.length))
return (selectedCircle)*600/circles.length
}
getAmountForNextSlice = (dx, offset) => {
// This just rounds to the nearest 200 to snap the circle to the correct thirds
const snappedOffset = this.snapOffset(offset);
// Depending on the direction, we either add 200 or subtract 200 to calculate new offset position. (200 are equal to 120deg!)
// const newOffset = dx > 0 ? snappedOffset + 200 : snappedOffset - 200; // fixed for 3 circles
const newOffset = dx > 0 ? snappedOffset + 600/circles.length : snappedOffset - 600/circles.length;
return newOffset;
}
snapOffset = (offset) => { return Math.round(offset / (600/circles.length)) * 600/circles.length; }
simplifyOffset = (val) => {
const { deltaAnim } = this.state
if(deltaAnim._offset > 600) deltaAnim.setOffset(deltaAnim._offset - 600)
if(deltaAnim._offset < -600) deltaAnim.setOffset(deltaAnim._offset + 600)
}
handleLayout = ({ nativeEvent }) => {
this.setState({
Radius: nativeEvent.layout.width,
container: {
height: nativeEvent.layout.height,
width: nativeEvent.layout.width
}
})
}
render() {
const {deltaAnim, radius} = this.state
return (
<Container
onLayout={this.handleLayout}
{...this._panResponder.panHandlers}
style={{
transform: [{
rotate: deltaAnim.interpolate({
inputRange: [-200, 0, 200],
outputRange: ['-120deg', '0deg', '120deg']
})
}]
}}
>
{circles.map((circle, index) => {
const {deltaTheta, Radius} = this.state
return (
<Circle
key={index}
color={circle.color}
radius={radius}
style={{
left: Math.sin(index*deltaTheta*Math.PI/180 + Math.PI)*Radius+this.offset(),
top: Math.cos(index*deltaTheta*Math.PI/180 + Math.PI)*Radius+this.offset(),
}}
>
<Text style={{color: 'white'}}>{index}</Text>
</Circle>
)
})}
</Container>
)
}
}
FYI: I got a solution. The result looks like this:
and the source code is given by:
import React, { Component } from 'react'
import { Text, View, PanResponder, Animated, Dimensions } from 'react-native'
import styled from 'styled-components'
import Circle from './Circle'
const SCREEN_WIDTH = Dimensions.get('window').width
const Container = styled(Animated.View)`
margin: auto;
width: 200px;
height: 200px;
position: relative;
top: 100px;
`
const gaussFunc = (x, sigma, mu) => {
return 1/sigma/Math.sqrt(2.0*Math.PI)*Math.exp(-1.0/2.0*Math.pow((x-mu)/sigma,2))
}
const myGaussFunc = (x) => gaussFunc(x, 1/2/Math.sqrt(2*Math.PI), 0)
const circles = [{
color: 'red'
}, {
color: 'blue'
}, {
color: 'green'
}, {
color: 'yellow'
}, {
color: 'purple'
}, {
color: 'black'
}, {
color: 'gray'
}, {
color: 'pink'
}, {
color: 'lime'
}, {
color: 'darkgreen'
}, {
color: 'crimson'
}, {
color: 'orange'
}, {
color: 'cyan'
}, {
color: 'navy'
}, {
color: 'indigo'
}, {
color: 'brown'
}, {
color: 'peru'
}]
function withFunction(callback) {
let inputRange = [], outputRange = [], steps = 50;
/// input range 0-1
for (let i=0; i<=steps; ++i) {
let key = i/steps;
inputRange.push(key);
outputRange.push(callback(key));
}
return { inputRange, outputRange };
}
export default class SDGCircle extends Component {
constructor(props) {
super(props)
const deltaTheta = 360/circles.length
const pxPerDeg = 200/120
const thetas = []
for (const i in circles) {
let val = i*deltaTheta*pxPerDeg
if(i >= 9)
val = -(circles.length-i)*deltaTheta*pxPerDeg
thetas.push(val)
}
this.state = {
deltaTheta,
Radius: 0, // radius of center circle (contaienr)
radius: 25, // radius of orbiting circles
container: { height: 0, width: 0 },
deltaAnim: new Animated.Value(0),
thetas,
thetasAnim: thetas.map(theta => new Animated.Value(theta)),
}
}
offset = () => parseInt(this.state.container.width/2)-this.state.radius
_panResponder = PanResponder.create({
nMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (event, gestureState) => true,
onPanResponderGrant: () => {
const { deltaAnim, thetasAnim, thetas } = this.state
deltaAnim.setOffset(deltaAnim._value)
deltaAnim.setValue(0)
const iSel = Math.round((deltaAnim._value+deltaAnim._offset)/(600/circles.length))
for(let i=0; i<circles.length; i++) {
let xi = i+iSel
if(xi > 16)
xi -= circles.length
if(xi < 0)
xi += circles.length
try {
thetasAnim[xi].setOffset(thetas[i])
} catch(err) {console.log(xi)}
}
},
onPanResponderMove: (event, gestureState) => {
const { deltaAnim, scaleAnim, deltaTheta, Radius, thetasAnim } = this.state
deltaAnim.setValue(gestureState.dx)
for (theta of thetasAnim) {
theta.setValue(-gestureState.dx)
}
},
onPanResponderRelease: (event, gestureState) => {
const {dx, vx} = gestureState
const {deltaAnim, thetasAnim, deltaTheta, thetas} = this.state
deltaAnim.flattenOffset()
const ithCircleValue = this.getIthCircleValue(dx, deltaAnim)
Animated.spring(deltaAnim, {
toValue: ithCircleValue,
friction: 5,
tension: 10,
}).start(() => {
this.simplifyOffset(deltaAnim)
});
}
})
getIthCircleValue = (dx, deltaAnim) => {
const selectedCircle = Math.round((deltaAnim._value+deltaAnim._offset)/(600/circles.length))
return (selectedCircle)*600/circles.length
}
snapOffset = (offset) => { return Math.round(offset / (600/circles.length)) * 600/circles.length; }
simplifyOffset = (anim) => {
if(anim._value + anim._offset >= 600) anim.setOffset(anim._offset - 600)
if(anim._value + anim._offset <= -600) anim.setOffset(anim._offset + 600)
}
handleLayout = ({ nativeEvent }) => {
this.setState({
Radius: nativeEvent.layout.width,
container: {
height: nativeEvent.layout.height,
width: nativeEvent.layout.width
}
})
}
render() {
const {deltaAnim, radius} = this.state
return (
<Container
onLayout={this.handleLayout}
{...this._panResponder.panHandlers}
style={{
transform: [{
rotate: deltaAnim.interpolate({
inputRange: [-200, 0, 200],
outputRange: ['-120deg', '0deg', '120deg']
})
}]
}}
>
{circles.map((circle, index) => {
const {deltaTheta, thetasAnim, Radius} = this.state
/* const difInPx = index*deltaTheta*200/120 */
let i = index
/* if(index >= Math.round(circles.length/2)) */
/* i = circles.length - index */
scale = thetasAnim[i].interpolate({
inputRange: [-300, 0, 300],
outputRange: [0, 2, 0],
})
return (
<Circle
key={index}
color={circle.color}
radius={radius}
style={{
left: Math.sin(index*deltaTheta*Math.PI/180 + Math.PI)*Radius+this.offset(),
top: Math.cos(index*deltaTheta*Math.PI/180 + Math.PI)*Radius+this.offset(),
transform: [{ scale }],
}}
>
<Text style={{color: 'white'}}>{index}</Text>
</Circle>
)
})}
</Container>
)
}
}
Related
Hi I need help with put value into bar with simbole % how de image
I try with install chartjs-plugin-datalabels but dont work.
The value I need is the value I get with which the bar is measured
The version is :
"chart.js": "^2.7.3",
"vue-chartjs": "^3.4.0",
I can't change the version becouse I have others graphics in these versions
My code
const horizonalLinePlugin = {
id: 'horizontalLine',
afterDraw: function (chartInstance) {
var yValue;
var yScale = chartInstance.scales["y-axis-0"];
var canvas = chartInstance.chart;
var ctx = canvas.ctx;
var index;
var line;
var style;
var fontSize = (200 / 114).toFixed(2);
ctx.font = fontSize + "em sans-serif";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
var data = ['20%', '30%', '40%', '60%', '10%', '30%', '20%'];
// var dataPresupuesto = ['10', '20', '30', '40', '10', '30', '20'];
var positionX = 130;
if (chartInstance.options.horizontalLine) {
for (index = 0; index < chartInstance.options.horizontalLine.length; index++) {
line = chartInstance.options.horizontalLine[index];
if (!line.style) {
style = "#080808";
} else {
style = line.style;
}
if (line.y) {
yValue = yScale.getPixelForValue(line.y);
} else {
yValue = 0;
}
ctx.lineWidth = 3;
if (yValue) {
window.chart = chartInstance;
ctx.beginPath();
ctx.moveTo(0, yValue);
ctx.lineTo(canvas.width, yValue);
ctx.strokeStyle = style;
ctx.stroke();
}
if (line.text) {
ctx.fillStyle = style;
ctx.fillText(line.text, 0, yValue + ctx.lineWidth);
}
}
return;
}
}
}
import { Bar } from 'vue-chartjs'
// import ChartJSPluginDatalabels from "chartjs-plugin-datalabels";
export default {
beforeMount() {
this.addPlugin(horizonalLinePlugin)
},
extends: Bar,
mounted() {
// Overwriting base render method with actual data.
this.renderChart({
labels: ['S01', 'S02', 'S03', 'S04', 'S05', 'S06', 'S07'],
datasets: [
{
label: 'Producción',
backgroundColor: '#000349',
data: [40, 39, 10, 40, 39, 80, 40]
},
{
label: 'Presupuesto',
backgroundColor: '#3AA9E0',
data: [40, 39, 10, 40, 39, 80, 40]
}
]
},
{
scales: {
xAxes: [{
stacked: true,
categoryPercentage: 0.5,
barPercentage: 1,
scaleLabel: {
display: true,
labelString: 'Semanas Comerciales'
}
}],
yAxes: [{
stacked: true,
}]
},
responsive: false,
plugins: {
datalabels: {
display: true,
color: "white",
textAlign: "center",
font: {
weight: "bold",
size: 16
}
}
}
})
}
}
Please someone that cant help me
https://valor-software.com/ng2-charts/#/LineChart
That is the link I followed for the line chart. I was able to show the data on the pie graph but it seems like different on this line chart. Like the graph below, how can I show data on the graph?
Update
I added a long line of code below. Typescript file can be also found in the the link above.
html
<div class="flex">
<div class="flex-item">
<div style="display: block;">
<canvas
baseChart
height="30vh"
width="70vw"
[datasets]="lineChartData"
[labels]="lineChartLabels"
[options]="lineChartOptions"
[colors]="lineChartColors"
[legend]="lineChartLegend"
[chartType]="lineChartType"
[plugins]="lineChartPlugins"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)"
></canvas>
</div>
</div>
</div>
ts
import { Component, OnInit, ViewChild } from "#angular/core";
import { BaseChartDirective, Color, Label } from "ng2-charts";
import { ChartDataSets, ChartOptions } from "chart.js";
import * as pluginAnnotations from "chartjs-plugin-annotation";
#Component({
selector: "kt-chart1",
templateUrl: "./chart1.component.html",
styleUrls: ["./chart1.component.scss"],
})
export class Chart1Component implements OnInit {
public lineChartData: ChartDataSets[] = [
{
data: [180, 33, 200, 300, 333, 270, 200],
label: "Cost",
yAxisID: "y-axis-1",
},
];
public lineChartLabels: Label[] = [
"01/05/2020",
"02/05/2020",
"03/05/2020",
"04/05/2020",
"05/05/2020",
"06/05/2020",
"07/05/2020",
];
public lineChartOptions: ChartOptions & { annotation: any } = {
plugins: {
labels: {
fontColor: ["green", "white", "red"],
precision: 2,
textShadow: true,
render: function (args) {
return args.value + "(" + args.toFixed(1) + "%)";
},
position: "inside",
},
datalabels: {
formatter: () => {
return null;
},
},
},
responsive: true,
scales: {
// We use this empty structure as a placeholder for dynamic theming.
xAxes: [{}],
yAxes: [
{
id: "y-axis-1",
position: "right",
gridLines: {
color: "rgba(255,0,0,0.3)",
},
ticks: {
fontColor: "red",
},
},
],
},
annotation: {
annotations: [
{
type: "line",
mode: "horizontal",
scaleID: "y-axis-1",
value: "200",
borderColor: "red",
borderWidth: 2,
label: {
enabled: true,
fontColor: "orange",
content: "200",
},
},
],
},
tooltips: {
callbacks: {
label: (item, data) => {
console.log(item);
return "Cost: " + item.xLabel + " " + item.yLabel;
},
},
},
};
public lineChartColors: Color[] = [
{
// grey
backgroundColor: "rgba(148,159,177,0.2)",
borderColor: "rgba(148,159,177,1)",
pointBackgroundColor: "rgba(148,159,177,1)",
pointBorderColor: "#fff",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: "rgba(148,159,177,0.8)",
},
{
// dark grey
backgroundColor: "rgba(77,83,96,0.2)",
borderColor: "rgba(77,83,96,1)",
pointBackgroundColor: "rgba(77,83,96,1)",
pointBorderColor: "#fff",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: "rgba(77,83,96,1)",
},
{
// red
backgroundColor: "rgba(255,0,0,0.3)",
borderColor: "red",
pointBackgroundColor: "rgba(148,159,177,1)",
pointBorderColor: "#fff",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: "rgba(148,159,177,0.8)",
},
];
public lineChartLegend = true;
public lineChartType = "line";
public lineChartPlugins = [pluginAnnotations];
#ViewChild(BaseChartDirective, { static: true }) chart: BaseChartDirective;
constructor() {}
ngOnInit() {
}
public randomize(): void {
for (let i = 0; i < this.lineChartData.length; i++) {
for (let j = 0; j < this.lineChartData[i].data.length; j++) {
this.lineChartData[i].data[j] = this.generateNumber(i);
}
}
this.chart.update();
}
private generateNumber(i: number) {
return Math.floor(Math.random() * (i < 2 ? 100 : 1000) + 1);
}
// events
public chartClicked({
event,
active,
}: {
event: MouseEvent;
active: {}[];
}): void {
console.log(event, active);
}
public chartHovered({
event,
active,
}: {
event: MouseEvent;
active: {}[];
}): void {
console.log(event, active);
}
public hideOne() {
const isHidden = this.chart.isDatasetHidden(1);
this.chart.hideDataset(1, !isHidden);
}
public pushOne() {
this.lineChartData.forEach((x, i) => {
const num = this.generateNumber(i);
const data: number[] = x.data as number[];
data.push(num);
});
this.lineChartLabels.push(`Label ${this.lineChartLabels.length}`);
}
public changeColor() {
this.lineChartColors[2].borderColor = "green";
this.lineChartColors[2].backgroundColor = `rgba(0, 255, 0, 0.3)`;
}
public changeLabel() {
this.lineChartLabels[2] = ["1st Line", "2nd Line"];
// this.chart.update();
}
}
Instead of using chartjs-plugin-annotation, you can draw the labels directly on the canvas using the Plugin Core API. It offers a number of hooks that can be used to perform custom code. In your case, you could use the afterDraw hook and register it in the ngOnInit method as follows:
ngOnInit(): void {
Chart.pluginService.register({
afterDraw: chart => {
var ctx = chart.chart.ctx;
var xAxis = chart.scales['x-axis-0'];
var yAxis = chart.scales['y-axis-0']; // would be 'y-axis-1' in your code sample
ctx.save();
chart.data.labels.forEach((l, i) => {
var value = chart.data.datasets[0].data[i];
var x = xAxis.getPixelForValue(l);
var y = yAxis.getPixelForValue(value);
ctx.textAlign = 'center';
ctx.font = '12px Arial';
ctx.fillStyle = 'red';
ctx.fillText(value, x, y - 15);
});
ctx.restore();
}
});
}
Please have a look at this StackBlitz to see how it works.
Hey everyone so I have a Konvajs application that works great as a video editor running on the vuejs library. However, I want to capture the canvas and create a seamless video at 60 fps. In order to do this, I am trying to utilize the CCapturejs library. It kind of works except for now the playback of the webm is really fast and still a bit choppy. Can any of ya'll look at this code and help me find the problem? Thanks.
<template>
<div>
<button #click="render">Render</button>
<button #click="stop">stop</button>
<h2>Backgrounds</h2>
<template v-for="background in backgrounds">
<img
:src="background.poster"
class="backgrounds"
#click="changeBackground(background.video)"
/>
</template>
<h2>Images</h2>
<template v-for="image in images">
<img
:src="image.source"
#click="addImage(image)"
class="images"
/>
</template>
<br />
<button #click="addText">Add Text</button>
<button v-if="selectedNode" #click="removeNode">
Remove selected {{ selectedNode.type }}
</button>
<label>Font:</label>
<select v-model="selectedFont">
<option value="Arial">Arial</option>
<option value="Courier New">Courier New</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Desoto">Desoto</option>
<option value="Kalam">Kalam</option>
</select>
<label>Font Size</label>
<input type="number" v-model="selectedFontSize" />
<label>Font Style:</label>
<select v-model="selectedFontStyle">
<option value="normal">Normal</option>
<option value="bold">Bold</option>
<option value="italic">Italic</option>
</select>
<label>Color:</label>
<input type="color" v-model="selectedColor" />
<button
v-if="selectedNode && selectedNode.type === 'text'"
#click="updateText"
>
Update Text
</button>
<template v-if="selectedNode && selectedNode.lottie">
<input type="text" v-model="text">
<button #click="updateAnim(selectedNode.image)">
Update Animation
</button>
</template>
<br />
<video
id="preview"
v-show="preview"
:src="preview"
:width="width"
:height="height"
preload="auto"
controls
/>
<a v-if="file" :href="file" download="dopeness.mp4">download</a>
<div id="container"></div>
</div>
</template>
<script>
import lottie from "lottie-web";
import CCapture from "../ccapture";
import * as anim from "../AEAnim/anim.json";
import * as anim2 from "../AEAnim/anim2.json";
import * as anim3 from "../AEAnim/anim3.json";
import * as anim4 from "../AEAnim/anim4.json";
import * as anim5 from "../AEAnim/anim5.json";
export default {
data() {
return {
source: null,
stage: null,
layer: null,
video: null,
animations: [],
text: "",
animationData: null,
captures: [],
capturer: null,
backgrounds: [
{
poster: "/api/files/stock/3oref310k1uud86w/poster/poster.jpg",
video:
"/api/files/stock/3oref310k1uud86w/main/1080/3oref310k1uud86w_1080.mp4"
},
{
poster: "https://i2.wp.com/livinglifefearless.co/wp-content/uploads/2019/08/Forrest-Gump-25-1.jpg?fit=1920%2C1220&ssl=1",
video: "/api/files/jedi/run_forest_run.mp4"
},
{
poster: "/api/files/stock/3yj2e30tk5x6x0ww/poster/poster.jpg",
video:
"/api/files/stock/3yj2e30tk5x6x0ww/main/1080/3yj2e30tk5x6x0ww_1080.mp4"
},
{
poster: "/api/files/stock/2ez931ik1mggd6j/poster/poster.jpg",
video:
"/api/files/stock/2ez931ik1mggd6j/main/1080/2ez931ik1mggd6j_1080.mp4"
},
{
poster: "/api/files/stock/yxrt4ej4jvimyk15/poster/poster.jpg",
video:
"/api/files/stock/yxrt4ej4jvimyk15/main/1080/yxrt4ej4jvimyk15_1080.mp4"
},
{
poster:
"https://images.costco-static.com/ImageDelivery/imageService?profileId=12026540&itemId=100424771-847&recipeName=680",
video: "/api/files/jedi/surfing.mp4"
},
{
poster:
"https://thedefensepost.com/wp-content/uploads/2018/04/us-soldiers-afghanistan-4308413-1170x610.jpg",
video: "/api/files/jedi/soldiers.mp4"
}
],
images: [
{ source: "/api/files/jedi/solo.jpg" },
{ source: "api/files/jedi/yoda.jpg" },
{ source: "api/files/jedi/yodaChristmas.jpg" },
{ source: "api/files/jedi/darthMaul.jpg" },
{ source: "api/files/jedi/darthMaul1.jpg" },
{ source: "api/files/jedi/trump.jpg" },
{ source: "api/files/jedi/hat.png" },
{ source: "api/files/jedi/trump.png" },
{ source: "api/files/jedi/bernie.png" },
{ source: "api/files/jedi/skywalker.png" },
{ source: "api/files/jedi/vader.gif" },
{ source: "api/files/jedi/vader2.gif" },
{ source: "api/files/jedi/yoda.gif" },
{ source: "api/files/jedi/kylo.gif" },
{
source: "https://media3.giphy.com/media/R3IxJW14a3QNa/source.gif",
animation: anim
},
{
source: "https://bestanimations.com/Text/Cool/cool-story-3.gif",
animation: anim2
},
{
source: "https://freefrontend.com/assets/img/css-text-animations/HTML-CSS-Animated-Text-Fill.gif",
animation: anim3
},
{
source: "api/files/jedi/slider.gif",
animation: anim4
},
{
source: "api/files/jedi/zoomer.gif",
animation: anim5
}
],
backgroundVideo: null,
imageGroups: [],
anim: null,
selectedNode: null,
selectedFont: "Arial",
selectedColor: "black",
selectedFontSize: 20,
selectedFontStyle: "normal",
width: 1920,
height: 1080,
texts: [],
preview: null,
file: null,
canvas: null
};
},
mounted: function() {
this.initCanvas();
},
methods: {
changeBackground(source) {
this.source = source;
this.video.src = this.source;
this.anim.stop();
this.anim.start();
this.video.play();
},
removeNode() {
if (this.selectedNode && this.selectedNode.type === "text") {
this.selectedNode.transformer.destroy(
this.selectedNode.text.transformer
);
this.selectedNode.text.destroy(this.selectedNode.text);
this.texts.splice(this.selectedNode.text.index - 1, 1);
this.selectedNode = null;
this.layer.draw();
} else if (this.selectedNode && this.selectedNode.type == "image") {
this.selectedNode.group.destroy(this.selectedNode);
this.imageGroups.splice(this.selectedNode.group.index - 1, 1);
if (this.selectedNode.lottie) {
cancelAnimationFrame(this.animations.animFrame);
this.selectedNode.lottie.destroy();
this.animations.splice(this.selectedNode.lottie.index - 1, 1);
}
this.selectedNode = null;
this.layer.draw();
}
},
async addImage(imageToAdd, isUpdate) {
let lottieAnimation = null;
let imageObj = null;
const type = imageToAdd.source.slice(imageToAdd.source.lastIndexOf("."));
const vm = this;
function process(img) {
return new Promise((resolve, reject) => {
img.onload = () => resolve({ width: img.width, height: img.height });
});
}
imageObj = new Image();
imageObj.src = imageToAdd.source;
imageObj.width = 200;
imageObj.height = 200;
await process(imageObj);
if (type === ".gif" && !imageToAdd.animation) {
const canvas = document.createElement("canvas");
canvas.setAttribute("id", "gif");
async function onDrawFrame(ctx, frame) {
ctx.drawImage(frame.buffer, frame.x, frame.y);
// redraw the layer
vm.layer.draw();
}
gifler(imageToAdd.source).frames(canvas, onDrawFrame);
canvas.onload = async () => {
canvas.parentNode.removeChild(canvas);
};
imageObj = canvas;
const gif = new Image();
gif.src = imageToAdd.source;
const gifImage = await process(gif);
imageObj.width = gifImage.width;
imageObj.height = gifImage.height;
} else if (imageToAdd.animation) {
if(!isUpdate){this.text = "new text";}
const canvas = document.createElement("canvas");
canvas.style.width = 1920;
canvas.style.height= 1080;
canvas.setAttribute("id", "animationCanvas");
const ctx = canvas.getContext("2d");
const div = document.createElement("div");
div.setAttribute("id", "animationContainer");
div.style.display = "none";
canvas.style.display = "none";
this.animationData = imageToAdd.animation.default;
for(let i =0; i <this.animationData.layers.length; i++){
for(let b =0; b<this.animationData.layers[i].t.d.k.length; b++){
this.animationData.layers[i].t.d.k[b].s.t = this.text;
}
}
lottieAnimation = lottie.loadAnimation({
container: div, // the dom element that will contain the animation
renderer: "svg",
loop: true,
autoplay: true,
animationData: this.animationData
});
lottieAnimation.imgSrc = imageToAdd.source;
lottieAnimation.text = this.text;
const svg = await div.getElementsByTagName("svg")[0];
async function updateSvg() {
const xml = new XMLSerializer().serializeToString(svg);
const svg64 = window.btoa(xml);
const b64Start = "data:image/svg+xml;base64,";
const image64 = b64Start + svg64;
imageObj = new Image({ width: canvas.width, height: canvas.height });
imageObj.src = image64;
await process(imageObj);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(imageObj, 0, 0, canvas.width, canvas.height);
vm.layer.draw();
updateSvg();
};
const animFrame = requestAnimationFrame(updateSvg);
this.animations.push({ lottie: lottieAnimation, animFrame });
imageObj = canvas;
canvas.onload = async () => {
canvas.parentNode.removeChild(canvas);
};
}
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
width: imageObj.width,
height: imageObj.height,
position: (0, 0),
strokeWidth: 10,
stroke: "blue",
strokeEnabled: false
});
const group = new Konva.Group({
draggable: true
});
// add the shape to the layer
addAnchor(group, 0, 0, "topLeft");
addAnchor(group, imageObj.width, 0, "topRight");
addAnchor(group, imageObj.width, imageObj.height, "bottomRight");
addAnchor(group, 0, imageObj.height, "bottomLeft");
imageObj = null;
image.on("click", function () {
vm.hideAllHelpers();
vm.selectedNode = {
type: "image",
group,
lottie: lottieAnimation,
image: imageToAdd
};
if(lottieAnimation && lottieAnimation.text){vm.text = lottieAnimation.text}
group.find("Circle").show();
vm.layer.draw();
});
image.on("mouseover", function(evt) {
if (vm.selectedNode && vm.selectedNode.type === "image") {
const index = image.getParent().index;
const groupId = vm.selectedNode.group.index;
if (index != groupId) {
evt.target.strokeEnabled(true);
vm.layer.draw();
}
} else {
evt.target.strokeEnabled(true);
vm.layer.draw();
}
});
image.on("mouseout", function(evt) {
evt.target.strokeEnabled(false);
vm.layer.draw();
});
vm.hideAllHelpers();
group.find("Circle").show();
group.add(image);
vm.layer.add(group);
vm.imageGroups.push(group);
vm.selectedNode = {
type: "image",
group,
lottie: lottieAnimation,
image: imageToAdd
};
function update(activeAnchor) {
const group = activeAnchor.getParent();
let topLeft = group.get(".topLeft")[0];
let topRight = group.get(".topRight")[0];
let bottomRight = group.get(".bottomRight")[0];
let bottomLeft = group.get(".bottomLeft")[0];
let image = group.get("Image")[0];
let anchorX = activeAnchor.getX();
let anchorY = activeAnchor.getY();
// update anchor positions
switch (activeAnchor.getName()) {
case "topLeft":
topRight.y(anchorY);
bottomLeft.x(anchorX);
break;
case "topRight":
topLeft.y(anchorY);
bottomRight.x(anchorX);
break;
case "bottomRight":
bottomLeft.y(anchorY);
topRight.x(anchorX);
break;
case "bottomLeft":
bottomRight.y(anchorY);
topLeft.x(anchorX);
break;
}
image.position(topLeft.position());
let width = topRight.getX() - topLeft.getX();
let height = bottomLeft.getY() - topLeft.getY();
if (width && height) {
image.width(width);
image.height(height);
}
}
function addAnchor(group, x, y, name) {
let stage = vm.stage;
let layer = vm.layer;
let anchor = new Konva.Circle({
x: x,
y: y,
stroke: "#666",
fill: "#ddd",
strokeWidth: 2,
radius: 8,
name: name,
draggable: true,
dragOnTop: false
});
anchor.on("dragmove", function() {
update(this);
layer.draw();
});
anchor.on("mousedown touchstart", function() {
group.draggable(false);
this.moveToTop();
});
anchor.on("dragend", function() {
group.draggable(true);
layer.draw();
});
// add hover styling
anchor.on("mouseover", function() {
let layer = this.getLayer();
document.body.style.cursor = "pointer";
this.strokeWidth(4);
layer.draw();
});
anchor.on("mouseout", function() {
let layer = this.getLayer();
document.body.style.cursor = "default";
this.strokeWidth(2);
layer.draw();
});
group.add(anchor);
}
},
async updateAnim(image){
this.addImage(image, true);
this.removeNode();
},
hideAllHelpers() {
for (let i = 0; i < this.texts.length; i++) {
this.texts[i].transformer.hide();
}
for (let b = 0; b < this.imageGroups.length; b++) {
this.imageGroups[b].find("Circle").hide();
}
},
render(){
this.video.currentTime =0;
this.video.loop = false;
this.captureCanvas();
},
captureCanvas (){
this.capturer = new CCapture( {
verbose: true,
//display: false,
framerate: 60,
//motionBlurFrames: 0,
//quality: 100,
format: "webm",
timeLimit: 0
//frameLimit: 0,
//autoSaveTime: 0,
} );
this.capturer.start();
this.updateCapturer();
},
stop(){
this.capturer.stop();
this.capturer.save();
},
updateCapturer(){
console.log("holy crap I was called");
console.log("this is the canvas", this.canvas);
let rAF;
if(this.capturer){
this.capturer.capture(this.canvas)
rAF = requestAnimationFrame(this.updateCapturer);
} else {
cancelAnimationFrame(rAf);
}
},
updateText() {
if (this.selectedNode && this.selectedNode.type === "text") {
const text = this.selectedNode.text;
const transformer = this.selectedNode.transformer;
text.fontSize(this.selectedFontSize);
text.fontFamily(this.selectedFont);
text.fontStyle(this.selectedFontStyle);
text.fill(this.selectedColor);
this.layer.draw();
}
},
addText() {
const vm = this;
const text = new Konva.Text({
text: "new text " + (vm.texts.length + 1),
x: 50,
y: 80,
fontSize: this.selectedFontSize,
fontFamily: this.selectedFont,
fontStyle: this.selectedFontStyle,
fill: this.selectedColor,
align: "center",
width: this.width * 0.5,
draggable: true
});
const transformer = new Konva.Transformer({
node: text,
keepRatio: true,
enabledAnchors: ["top-left", "top-right", "bottom-left", "bottom-right"]
});
text.on("click", async () => {
for (let i = 0; i < this.texts.length; i++) {
let item = this.texts[i];
if (item.index === text.index) {
let transformer = item.transformer;
this.selectedNode = { type: "text", text, transformer };
this.selectedFontSize = text.fontSize();
this.selectedFont = text.fontFamily();
this.selectedFontStyle = text.fontStyle();
this.selectedColor = text.fill();
vm.hideAllHelpers();
transformer.show();
transformer.moveToTop();
text.moveToTop();
vm.layer.draw();
break;
}
}
});
text.on("mouseover", () => {
transformer.show();
this.layer.draw();
});
text.on("mouseout", () => {
if (
(this.selectedNode &&
this.selectedNode.text &&
this.selectedNode.text.index != text.index) ||
(this.selectedNode && this.selectedNode.type === "image") ||
!this.selectedNode
) {
transformer.hide();
this.layer.draw();
}
});
text.on("dblclick", () => {
text.hide();
transformer.hide();
vm.layer.draw();
let textPosition = text.absolutePosition();
let stageBox = vm.stage.container().getBoundingClientRect();
let areaPosition = {
x: stageBox.left + textPosition.x,
y: stageBox.top + textPosition.y
};
let textarea = document.createElement("textarea");
window.document.body.appendChild(textarea);
textarea.value = text.text();
textarea.style.position = "absolute";
textarea.style.top = areaPosition.y + "px";
textarea.style.left = areaPosition.x + "px";
textarea.style.width = text.width() - text.padding() * 2 + "px";
textarea.style.height = text.height() - text.padding() * 2 + 5 + "px";
textarea.style.fontSize = text.fontSize() + "px";
textarea.style.border = "none";
textarea.style.padding = "0px";
textarea.style.margin = "0px";
textarea.style.overflow = "hidden";
textarea.style.background = "none";
textarea.style.outline = "none";
textarea.style.resize = "none";
textarea.style.lineHeight = text.lineHeight();
textarea.style.fontFamily = text.fontFamily();
textarea.style.transformOrigin = "left top";
textarea.style.textAlign = text.align();
textarea.style.color = text.fill();
let rotation = text.rotation();
let transform = "";
if (rotation) {
transform += "rotateZ(" + rotation + "deg)";
}
let px = 0;
let isFirefox =
navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
if (isFirefox) {
px += 2 + Math.round(text.fontSize() / 20);
}
transform += "translateY(-" + px + "px)";
textarea.style.transform = transform;
textarea.style.height = "auto";
textarea.focus();
// start
function removeTextarea() {
textarea.parentNode.removeChild(textarea);
window.removeEventListener("click", handleOutsideClick);
text.show();
transformer.show();
transformer.forceUpdate();
vm.layer.draw();
}
function setTextareaWidth(newWidth) {
if (!newWidth) {
// set width for placeholder
newWidth = text.placeholder.length * text.fontSize();
}
// some extra fixes on different browsers
let isSafari = /^((?!chrome|android).)*safari/i.test(
navigator.userAgent
);
let isFirefox =
navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
if (isSafari || isFirefox) {
newWidth = Math.ceil(newWidth);
}
let isEdge =
document.documentMode || /Edge/.test(navigator.userAgent);
if (isEdge) {
newWidth += 1;
}
textarea.style.width = newWidth + "px";
}
textarea.addEventListener("keydown", function(e) {
// hide on enter
// but don't hide on shift + enter
if (e.keyCode === 13 && !e.shiftKey) {
text.text(textarea.value);
removeTextarea();
}
// on esc do not set value back to node
if (e.keyCode === 27) {
removeTextarea();
}
});
textarea.addEventListener("keydown", function(e) {
let scale = text.getAbsoluteScale().x;
setTextareaWidth(text.width() * scale);
textarea.style.height = "auto";
textarea.style.height =
textarea.scrollHeight + text.fontSize() + "px";
});
function handleOutsideClick(e) {
if (e.target !== textarea) {
text.text(textarea.value);
removeTextarea();
}
}
setTimeout(() => {
window.addEventListener("click", handleOutsideClick);
});
// end
});
text.transformer = transformer;
this.texts.push(text);
this.layer.add(text);
this.layer.add(transformer);
this.hideAllHelpers();
this.selectedNode = { type: "text", text, transformer };
transformer.show();
this.layer.draw();
},
initCanvas() {
const vm = this;
this.stage = new Konva.Stage({
container: "container",
width: vm.width,
height: vm.height
});
this.layer = new Konva.Layer();
this.stage.add(this.layer);
let video = document.createElement("video");
video.setAttribute("id", "video");
video.setAttribute("ref", "video");
if (this.source) {
video.src = this.source;
}
video.preload = "auto";
video.loop = "loop";
video.style.display = "none";
this.video = video;
this.backgroundVideo = new Konva.Image({
image: vm.video,
draggable: false
});
this.video.addEventListener("loadedmetadata", function(e) {
vm.backgroundVideo.width(vm.width);
vm.backgroundVideo.height(vm.height);
});
this.video.addEventListener("ended", () => {
console.log("the video ended");
this.stop();
this.capturer = null;
this.anim.stop();
this.anim.start();
this.video.loop = "loop";
this.video.play();
});
this.anim = new Konva.Animation(function() {
console.log("animation called");
// do nothing, animation just need to update the layer
}, vm.layer);
this.layer.add(this.backgroundVideo);
this.layer.draw();
const canvas = document.getElementsByTagName("canvas")[0];
canvas.style.border = "3px solid red";
this.canvas = canvas;
}
}
};
</script>
<style scoped>
body {
margin: 0;
padding: 0;
background-color: #f0f0f0;
}
.backgrounds,
.images {
width: 100px;
height: 100px;
padding-left: 2px;
padding-right: 2px;
}
</style>
Use this code to pause the video and take screenshots. You can than use Whammy to generate a webm and convert it to whatever file format you like with ffmpeg
generateVideo(){
const vid = new Whammy.fromImageArray(this.captures, 30);
vid.name = "project_id_238.webm";
vid.lastModifiedDate = new Date();
this.file = URL.createObjectURL(vid);
},
async pauseAll(){
this.pauseVideo();
if(this.animations.length){
this.pauseLotties()
}
this.captures.push(this.canvas.toDataURL('image/webp'));
if(!this.ended){
setTimeout(()=>{
this.pauseAll();
}, 500);
}
},
async pauseVideo(){
console.log("curretTime",this.video.currentTime);
console.log("duration", this.video.duration);
this.video.pause();
const oneFrame = 1/30;
this.video.currentTime += oneFrame;
},
async pauseLotties(){
lottie.freeze();
for(let i =0; i<this.animations.length; i++){
let step =0;
let animation = this.animations[i].lottie;
if(animation.currentFrame<=animation.totalFrames){
step = animation.currentFrame + animation.totalFrames/30;
}
lottie.goToAndStop(step, true, animation.name);
}
},
Android: I am not able to get the file on the path, I am using the latest npm version 0.5.0
My path looks like "file:data/data/com.sampleproject/cache/727b3cdc-2ff1-4918-8a71-f00be535e4d9imagemarker.png"
The cache folder is blank, even there is no hidden files.
markPosition() {
//alert(this.state.signature);
this.state.signature =
Platform.OS === 'android'
? 'file://' + this.state.signature
: this.state.signature;
Marker.markText({
src: this.state.signature,
text: this.state.data,
position: 'bottomRight',
color: '#FF0000',
fontName: 'Arial-BoldItalicMT',
fontSize: 44,
scale: 1,
quality: 100,
markerScale: 1,
shadowStyle: {
dx: 10.5,
dy: 20.8,
radius: 20.9,
color: 'transparent',
},
textBackgroundStyle: {
paddingX: 10,
paddingY: 10,
color: 'transparent',
},
saveFormat: this.state.saveFormat,
})
.then(path => {
// alert('saved path');
console.log('path1241asdjaksdjakdqweqw', path);
this.setState(
{
signature: path,
},
() => {
if (this.state.signature !== null) {
// console.log('dsdsfs4234242', this.state.signature);
//Orientation.lockToPortrait();
const getSignatureValue = this.props.navigation.getParam(
'getSignature',
);
getSignatureValue(path);
// this.props.navigation.push('NALGSample', {
// signed: this.state.signature,
// });
Orientation.lockToPortrait();
this.props.navigation.pop();
}
},
);
})
.catch(err => {
console.log('====================================');
// alert(err + 'err');
});
}
Environment
"react-native": "0.61.4",
"react-native-image-marker": "^0.6.1",
Good morning, I find myself working with the Konvajs library, https://github.com/konvajs/vue-konva
There is the following documentation: https://konvajs.org/docs/sandbox/Connected_Objects.html, but I can't implement it with vuejs
Since what I need to do is that when selecting object 1, I can drag and form the arrow and when selecting object 2, they are linked
Currently I have built the following:
<template>
<v-container>
<v-stage :config="configKonva">
<v-layer>
<v-circle :config="configCircle"></v-circle>
</v-layer>
<v-layer>
<v-circle :config="configCircleA"></v-circle>
</v-layer>
</v-stage>
</v-container>
</template>
<script>
export default {
data(){
return {
configKonva: {
width: 200,
height: 200
},
configCircle: {
x: 100,
y: 100,
radius: 70,
fill: "red",
stroke: "black",
strokeWidth: 4,
draggable: true
},
configCircleA: {
x: 100,
y: 100,
radius: 70,
fill: "green",
stroke: "black",
strokeWidth: 4,
draggable: true
}
}
},
}
</script>
Visually I have only created the circles, I lack the connection of these 2 through a line
There are many ways to implement such functionality. Basically, you just need to listen to mousedown, mousemove and mouseup events to understand when to draw lines. You can also add touchstart, touchmove and touchend events to support mobile devices:
<template>
<div>
<v-stage
ref="stage"
:config="stageSize"
#mousedown="handleMouseDown"
#mouseup="handleMouseUp"
#mousemove="handleMouseMove"
>
<v-layer>
<v-line
v-for="line in connections"
:key="line.id"
:config="{
stroke: 'black',
points: line.points
}"
/>
<v-circle
v-for="target in targets"
:key="target.id"
:config="{
x: target.x,
y: target.y,
radius: 40,
stroke: 'black',
fill: 'green'
}"
/>
<v-text :config="{ text: 'Try to drag-to-connect objects'}"/>
</v-layer>
<v-layer ref="dragLayer"></v-layer>
</v-stage>
</div>
</template>
<script>
import Konva from "konva";
const width = window.innerWidth;
const height = window.innerHeight;
let vm = {};
function generateTargets() {
const circles = [];
for (var i = 0; i < 10; i++) {
circles.push({
x: width * Math.random(),
y: height * Math.random(),
id: i
});
}
return circles;
}
export default {
data() {
return {
stageSize: {
width: width,
height: height
},
targets: generateTargets(),
connections: [],
drawningLine: false
};
},
methods: {
handleMouseDown(e) {
const onCircle = e.target instanceof Konva.Circle;
if (!onCircle) {
return;
}
this.drawningLine = true;
this.connections.push({
id: Date.now(),
points: [e.target.x(), e.target.y()]
});
},
handleMouseMove(e) {
if (!this.drawningLine) {
return;
}
const pos = e.target.getStage().getPointerPosition();
const lastLine = this.connections[this.connections.length - 1];
lastLine.points = [lastLine.points[0], lastLine.points[1], pos.x, pos.y];
},
handleMouseUp(e) {
const onCircle = e.target instanceof Konva.Circle;
if (!onCircle) {
return;
}
this.drawningLine = false;
const lastLine = this.connections[this.connections.length - 1];
lastLine.points = [
lastLine.points[0],
lastLine.points[1],
e.target.x(),
e.target.y()
];
}
}
};
</script>
DEmo: https://codesandbox.io/s/vue-konva-connection-objects-qk2ps