Assigning a variable length to setInterval() on a per-case basis - javascript

I'm currently trying to adapt this project by Nutty7t into a more versatile simulation of Persona 5's SMS system. I have made progress given my previous experiences designing websites (in High School I revamped by history teacher's in-class sites).
As of now I have implemented a system that simulates multiple individuals participating in the text conversation. Each message has a variable identifying who it is sent from, and it grabs the corresponding character's text icon to display.
There are two things I have yet to simulate:
First off is how long it is before the next message - as it stands all messages are displayed 2000ms apart. It works but it feels off, and I'd like to figure out how to set the interval on a per message basis.
Second, some indication participant(s) are typing.
To start off the system has 3 files. One .css stylesheet, the .html page, and the .js script.
.css stylesheet
#import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro&display=swap");
#font-face {
font-family: "Optima Nova";
src: url('fonts/eot/OptimaNovaLT-Black.eot');
src: url('fonts/eot/OptimaNovaLT-Black.eot') format('embedded-opentype'),
url('fonts/woff2/OptimaNovaLT-Black.woff2') format('woff2'),
url('fonts/woff/OptimaNovaLT-Black.woff') format('woff'),
url('fonts/ttf/OptimaNovaLT-Black.ttf') format('truetype');
}
#chatBody * {
overflow-anchor: none;
}
#anchor {
overflow-anchor: auto;
height: 10px;
}
html {
background: #cc0000;
font-family: "Optima Nova","Source Sans Pro", sans-serif;
font-weight: 700;
}
body {
max-width: 600px;
margin: auto;
height: 100.001vh;
text-align: center;
}
a:link {
color:rgb(46, 46, 46);
text-decoration: none;
}
.flipX {
transform: scaleX(-1);
transform-origin: center;
}
.html file
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>Persona SMS Simulation</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="chatBody">
<!-- partial:index.partial.html -->
<img src="images/scrolling.png">
<div id="Credit">Orignal project by nutty7t.<br>Edited by CerebralPolicy (u/harperofthefreenorth)</div>
<div id="chat">
<!--insert new-->
</div>
<script id="chat-thread" type="x-template">
<div v-if="window.devicePixelRatio === 1">
<br/>
<ChatMessage
v-for="message in messages"
ref="chatMessages"
:message="message.text"
:remote="message.remote"
:chatter="message.chatter"
:consecutive="message.consecutive"
/>
</div>
<div v-else style="margin: 20px; color: white">
<h1>Oops, my bad...</h1>
<p>
Your <code>window.devicePixelRatio</code> does not equal <code>1</code>. This means that the ratio between physical pixels and logical pixels don't have a 1-to-1 correspondence on your display device. I have yet to (and probably never will) implement pixel scaling for these ratios. If you're on a mobile device, try viewing this on a desktop computer; but not on a Retina or HiDPI display.
</p>
<p>- (nutty7t)</p>
</div>
</script>
<script id="chat-message" type="x-template">
<svg xmlns="http://www.w3.org/2000/svg" :viewBox="`0 0 500 ${viewBoxHeight}`" :style="style">
<!--
HACK: This invisible <text> element is used to figure out
the bounding box of a line of text without it being painted.
-->
<text
ref="hackText"
visibility="hidden"
:style="{ fontSize: fontSize + 'px' }">{{ hackText }}</text>
<!-- Buddy Avatar -->
<a v-if="newChatter">
<polygon
v-if="remote"
points="-10,-5 62,5 70,55 5,63"
:transform="`translate(30,${containerHeight / 2 + messageBox.origin.y - 25})`"
style="fill: black"/>
<polygon
v-if="remote"
points="0,0 60,10 70,50 10,60"
:transform="`translate(30,${containerHeight / 2 + messageBox.origin.y - 25})`"
style="fill: white"/>
<clipPath id="avatarClipPath">
<polygon points="2,-10 62,-10 100,42 10,58"/>
</clipPath>
<!-- absolute dogshit workaround -->
<image
clip-path="url(#avatarClipPath)"
v-if="chatter == 'Akechi'"
x="-10"
y="-10"
xlink:href="images\Akechi.png"
:transform="`translate(30,${containerHeight / 2 + messageBox.origin.y - 25})`"
width="80px"
/>
<image
clip-path="url(#avatarClipPath)"
v-else-if="chatter == 'Ann'"
x="-10"
y="-10"
xlink:href="images\Ann.png"
:transform="`translate(30,${containerHeight / 2 + messageBox.origin.y - 25})`"
width="80px"
/>
<image
clip-path="url(#avatarClipPath)"
v-else-if="chatter == 'Futaba'"
x="-10"
y="-10"
xlink:href="images\Futaba.png"
:transform="`translate(30,${containerHeight / 2 + messageBox.origin.y - 25})`"
width="80px"
/>
<image
clip-path="url(#avatarClipPath)"
v-else-if="chatter == 'Haru'"
x="-10"
y="-10"
xlink:href="images\Haru.png"
:transform="`translate(30,${containerHeight / 2 + messageBox.origin.y - 25})`"
width="80px"
/>
<image
clip-path="url(#avatarClipPath)"
v-else-if="chatter == 'Hifumi'"
x="-10"
y="-10"
xlink:href="images\Hifumi.png"
:transform="`translate(30,${containerHeight / 2 + messageBox.origin.y - 25})`"
width="80px"
/>
<image
clip-path="url(#avatarClipPath)"
v-else-if="chatter == 'Kasumi'"
x="-10"
y="-10"
xlink:href="images\Kasumi.png"
:transform="`translate(30,${containerHeight / 2 + messageBox.origin.y - 25})`"
width="80px"
/>
<image
clip-path="url(#avatarClipPath)"
v-else-if="chatter == 'Makoto'"
x="-10"
y="-10"
xlink:href="images\Thing.png"
:transform="`translate(30,${containerHeight / 2 + messageBox.origin.y - 25})`"
width="80px"
/>
<image
clip-path="url(#avatarClipPath)"
v-else-if="chatter == 'Ryuji'"
x="-10"
y="-10"
xlink:href="images\Ryuji.png"
:transform="`translate(30,${containerHeight / 2 + messageBox.origin.y - 25})`"
width="80px"
/>
<image
clip-path="url(#avatarClipPath)"
v-else-if="chatter == 'Sumire'"
x="-10"
y="-10"
xlink:href="images\Sumire.png"
:transform="`translate(30,${containerHeight / 2 + messageBox.origin.y - 25})`"
width="80px"
/>
<image
clip-path="url(#avatarClipPath)"
v-else-if="chatter == 'Yuskue'"
x="-10"
y="-10"
xlink:href="images\Yuskue.png"
:transform="`translate(30,${containerHeight / 2 + messageBox.origin.y - 25})`"
width="80px"
/>
<image
clip-path="url(#avatarClipPath)"
v-else-if="chatter == 'Lavensa'"
x="-10"
y="-10"
xlink:href="images\Lavensa.png"
:transform="`translate(30,${containerHeight / 2 + messageBox.origin.y - 25})`"
width="80px"
/>
</a>
<!-- Message Text Container Border -->
<polygon
:points="containerBorderPoints"
:style="{ fill: primaryColor }"
:class="{ flipX: !remote }"/>
<!-- Message Text Container Tail Border -->
<polygon
:points="containerTailBorderPoints"
:style="{ fill: primaryColor }"
:class="{ flipX: !remote }"/>
<!-- Message Text Container Tail -->
<polygon
:points="containerTailPoints"
:style="{ fill: secondaryColor }"
:class="{ flipX: !remote }"/>
<!-- Message Text Container -->
<polygon
:points="containerPoints"
:style="{ fill: secondaryColor }"
:class="{ flipX: !remote }"/>
<!-- Message Text -->
<text :y="textOffset.y" :style="{ fontSize: fontSize + 'px' }">
<tspan
v-for="line of wrappedMessage"
:x="remote ? messageBox.origin.x + textOffset.x : 500 - messageBox.origin.x - messageBox.centerWidth"
:dy="`${lineHeight}em`"
:style="{ fill: primaryColor }">
{{ line.text }}
</tspan>
</text>
</svg>
</script>
<!-- partial -->
<div id="anchor"></div>
<script src='https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js'></script>
<script src='https://unpkg.com/vue-async-computed#3.7.0'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.3/TweenMax.min.js'></script>
<script src='//cdnjs.cloudflare.com/ajax/libs/gsap/latest/plugins/ScrollToPlugin.min.js'></script><script src="./script.js"></script>
</div>
</body>
</html>
.js script
const ChatMessage = {
template: '#chat-message',
props: {
message: {
type: String,
required: true
},
remote: {
// Comment by Nutty7t:
// Does the message originate from a remote source?
type: Boolean,
default: false
},
chatter: {
// Used to grab chat image
type: String
},
newChatter: {
// TODO: Hide picture if same chatter sends multiple messages in a row
type: Boolean,
default: true
},
messageWait: {
// How long before the next message
type: Number,
default: 2000 // in ms
},
fontSize: {
type: Number,
default: 14 },
lineHeight: {
type: Number,
default: 1.5 // em
} },
data() {
return {
hackText: '',
style: {
opacity: 0 } };
},
computed: {
// Comment by Nutty7t:
// ------------------------------------------
// Message Box (remote: true)
// ------------------------------------------
//
// origin x - right width
// \ [ ---- center width ---- ] |
// x----------------------- x --- x
// / | <message text> | /
// / | | /
// x --- x ---------------------- x
// |
// + - left width
//
messageBox() {
return {
origin: {
x: this.remote ? 130 : 60,
y: 20 },
centerWidth: 300,
leftWidth: 10,
rightWidth: 20,
slantHeight: 5,
border: {
normal: 4,
left: 15,
right: 35 } };
},
textOffset() {
return {
// Comment by Nutty7t:
// Left padding.
x: 15,
// Adjust for top/bottom padding.
y: this.messageBox.origin.y + this.fontSize * this.lineHeight / 4 };
},
containerPoints() {
return [
{
x: this.messageBox.origin.x,
y: this.messageBox.origin.y },
{
x: this.messageBox.origin.x + this.messageBox.centerWidth + this.messageBox.rightWidth,
y: this.messageBox.origin.y },
{
x: this.messageBox.origin.x + this.messageBox.centerWidth,
y: this.messageBox.origin.y + this.containerHeight + this.messageBox.slantHeight },
{
x: this.messageBox.origin.x - this.messageBox.leftWidth,
y: this.messageBox.origin.y + this.containerHeight }].
map(p => `${p.x},${p.y}`).join(' ');
},
containerBorderPoints() {
return [
{
x: this.messageBox.origin.x - this.messageBox.border.normal,
y: this.messageBox.origin.y - this.messageBox.border.normal },
{
x: this.messageBox.origin.x + this.messageBox.centerWidth + this.messageBox.border.right,
y: this.messageBox.origin.y - this.messageBox.border.normal },
{
x: this.messageBox.origin.x + this.messageBox.centerWidth + this.messageBox.border.normal,
y: this.messageBox.origin.y + this.containerHeight + this.messageBox.border.normal + this.messageBox.slantHeight },
{
x: this.messageBox.origin.x - this.messageBox.border.left,
y: this.messageBox.origin.y + this.containerHeight + this.messageBox.border.normal }].
map(p => `${p.x},${p.y}`).join(' ');
},
containerTailPoints() {
return [
{
x: this.messageBox.origin.x - 33,
y: this.messageBox.origin.y + this.containerHeight / 2 + 8 },
{
x: this.messageBox.origin.x - 17,
y: this.messageBox.origin.y + this.containerHeight / 2 - 10 },
{
x: this.messageBox.origin.x - 12,
y: this.messageBox.origin.y + this.containerHeight / 2 - 4 },
{
x: this.messageBox.origin.x,
y: this.messageBox.origin.y + this.containerHeight / 2 - 10 },
{
x: this.messageBox.origin.x,
y: this.messageBox.origin.y + this.containerHeight / 2 + 5 },
{
x: this.messageBox.origin.x - 18,
y: this.messageBox.origin.y + this.containerHeight / 2 + 10 },
{
x: this.messageBox.origin.x - 22,
y: this.messageBox.origin.y + this.containerHeight / 2 + 5 }].
map(p => `${p.x},${p.y}`).join(' ');
},
containerTailBorderPoints() {
return [
{
x: this.messageBox.origin.x - 40,
y: this.messageBox.origin.y + this.containerHeight / 2 + 12 },
{
x: this.messageBox.origin.x - 15,
y: this.messageBox.origin.y + this.containerHeight / 2 - 16 },
{
x: this.messageBox.origin.x - 12,
y: this.messageBox.origin.y + this.containerHeight / 2 - 10 },
{
x: this.messageBox.origin.x,
y: this.messageBox.origin.y + this.containerHeight / 2 - 15 },
{
x: this.messageBox.origin.x,
y: this.messageBox.origin.y + this.containerHeight / 2 + 10 },
{
x: this.messageBox.origin.x - 20,
y: this.messageBox.origin.y + this.containerHeight / 2 + 15 },
{
x: this.messageBox.origin.x - 24,
y: this.messageBox.origin.y + this.containerHeight / 2 + 10 }].
map(p => `${p.x},${p.y}`).join(' ');
},
containerHeight() {
// Comment by Nutty7t:
// Compute how much vertical space the message text takes up by
// multiplying the line height by the number of lines in the message.
let height = this.fontSize * this.lineHeight * this.wrappedMessage.length;
// Comment by Nutty7t:
// Now, we need to add some extra bottom padding otherwise the
// descenders (the part of the characters beneath the baseline)
// will get clipped. I don't know the exact height of the descender,
// but I figure that 1/2 em should be fine. And then we'll add another
// 1/4 em for top and bottom paddings (1/2 em in total).
//
// ---
// | top padding (1/4 em)
// ---
// | text height (line height * # of lines)
// ---
// | descender padding (1/2 em)
// ---
// . | slanted bottom edge (this.messageBox.slantHeight)
// ---
// | bottom padding (1/4 em)
// ---
//
return height + this.fontSize * this.lineHeight;
},
viewBoxHeight() {
// Comment by Nutty7t:
//
// ---
// | border width
// ---
// | container height
// ---
// | border width
// ---
//
return this.containerHeight + this.messageBox.origin.y * 2;
},
primaryColor() {
return this.remote ? 'white' : 'black';
},
secondaryColor() {
return this.remote ? 'black' : 'white';
} },
asyncComputed: {
wrappedMessage: {
async get() {
// Comment by Nutty7t:
// Kind of a hacky way of implementing word wrapping
// on SVG <text> elements. Not quite sure how to go
// about determining the bounding box of some text,
// without actually rendering it on the DOM.
const words = this.message.split(/\s+/);
const lines = [];
let line = [];
while (words.length > 0) {
line.push(words.shift());
this.hackText = line.join(' ');
if ((await this.hackTextWidth()) > this.messageBox.centerWidth) {
words.unshift(line.pop());
lines.push({ text: line.join(' ') });
line = [];
}
}
lines.push({ text: line.join(' ') });
if (lines.length === 1) {
// Messages that are only one line have a fluid width.
this.messageBox.centerWidth = (await this.hackTextWidth()) + this.textOffset.x * 2;
}
return lines;
},
default: [] } },
methods: {
async hackTextWidth() {
// Wait until #hackText is rendered in the DOM.
while (!this.$refs.hackText) {
await Vue.nextTick();
}
// Wait for Vue to update the innerHTML of #hackText.
await Vue.nextTick();
if (this.$refs.hackText.innerHTML === this.hackText) {
return this.$refs.hackText.clientWidth;
} else {
console.log(
`[error] hackText does not have expected text\n` +
` expected: "${this.hackText}"\n` +
` actual: "${this.$refs.hackText.innerHTML}"`);
return 0;
}
} },
mounted() {
TweenMax.to(this.style, 1, {
opacity: 1,
ease: Power3.easeOut });
} };
const ChatThread = new Vue({
el: '#chat',
template: '#chat-thread',
components: { ChatMessage },
data() {
return {
messages: [],
queue: [
//format
// text: what is sent [nutty7t]
// chatter: who sent it [cerebralpolicy]
// remote: is it someone other than POV [nutty7t]
// messageWait: how long before next reply [cerebralpolicy]
{
text: "Message 1",
chatter: 'Haru',
remote: true,
messageWait: 1000
},
{
text: 'Message 2',
chatter: 'Ryuji',
remote: true,
messageWait: 2000
},
{
text: "Message 3",
chatter: 'Makoto',
remote: true,
messageWait: 1500
},
],
interval: undefined };
},
watch: {
async messages(newMessages, oldMessages) {
if (this.queue.length === 0) {
clearInterval(this.interval);
}
await Vue.nextTick();
const messages = this.$refs.chatMessages;
const lastMessage = messages[messages.length - 1];
// possible mechanisms?
// const thisMessage = messages[messages.length];
// var delayMessage = thisMessage.messageWait;
// display indicator someone is typing
if (document.body !== null) {
TweenMax.to(window, 1, { scrollTo: { y: document.body.scrollHeight }, ease: Power3.easeOut });
}
} },
mounted() {
this.interval = setInterval(() => {
this.messages.push(this.queue.shift());
}, 2000); // Need to pass messageWait value here
}
});
document.scrollingElement.scroll(0, 1);
My instinct was to format the chatThread block as follows.
const ChatThread = new Vue({
el: '#chat',
template: '#chat-thread',
components: { ChatMessage },
data() {
return {
messages: [],
queue: [
//format
// text: what is sent [nutty7t]
// chatter: who sent it [cerebralpolicy]
// remote: is it someone other than POV [nutty7t]
// messageWait: how long before next reply [cerebralpolicy]
{
text: "Message 1",
chatter: 'Haru',
remote: true,
messageWait: 1000 // I want to make Ryuji wait 1000ms before responding
},
{
text: 'Message 2',
chatter: 'Ryuji',
remote: true,
messageWait: 2000 // I want to make Makoto wait 2000ms before responding
},
{
text: "Message 3",
chatter: 'Makoto',
remote: true,
messageWait: 1500 // and so on
},
],
interval: undefined };
},
watch: {
async messages(newMessages, oldMessages) {
if (this.queue.length === 0) {
clearInterval(this.interval);
}
await Vue.nextTick();
const messages = this.$refs.chatMessages;
const lastMessage = messages[messages.length - 1];
// possible mechanisms?
// const thisMessage = messages[messages.length];
// var delayMessage = thisMessage.messageWait;
// display indicator someone is typing
if (document.body !== null) {
TweenMax.to(window, 1, { scrollTo: { y: document.body.scrollHeight }, ease: Power3.easeOut });
}
} },
mounted() {
this.interval = setInterval(() => {
this.messages.push(this.queue.shift());
}, this.lastMessage.messageWait);
// I expected that it would break the first message but instead it appears to grab a value of 0ms
// all messages display instantly. The first message doesn't break, surprisingly.
}
});
I'm not sure if javascript can do what I have outlined. I highly suspect it can but I am not familiar with it nor the Vue template system. Can anyone assist me?

Related

How can I render circles around rectangle in Konva js?

I need to render table and chairs like in this picture:
I dont know how to calculate position of circles around rect. I tried some code, but its not working...Anybody knows how to solve it?
Check my code:
let smallCircleRadius = 20
let tableSize = {
width: ((seats-4/2)*(smallCircleRadius)),
height: ((seats-4/2)*(smallCircleRadius))
}
let controlY = 0
let totalCircleSide = (seats-4)/2
let controlX = 0
let distanceY = 0
let distanceX = 0
let table = new Konva.Rect({
width: tableSize.width,
height: tableSize.height,
fill: '#fff',
stroke: '#c3c6cf',//'#b2cfcf',
strokeWidth: 8,
x:150,
y: 150
});
let count = 0
group.add(table)
for (var i = 0; i < seats; i++) {
// let distanceToTable = tableSize.width/2;
// let x = i <= 2 ? table.x() + distanceToTable * i + (smallCircleRadius + 8) : count > totalCircleSide ? distanceToTable + distanceX+smallCircleRadius: controlX
// let y = i < 2 ? table.y() - distanceToTable/2: count > totalCircleSide ? controlY : distanceToTable + distanceY*smallCircleRadius
//let x = table.x()
//let y = table.y()
group.add(new Konva.Circle({ radius: smallCircleRadius, fill: '#d2d6df', stroke: '#c3c6cf',strokeWidth: 3, x, y }));
}
Make yourself a simple model that describes the position of the circles in simple relationship of circles to the table. Something like this can be extended via different models to accommodate other table layouts as the description of the layout is entirely in the model data.
const
// Set up a canvas stage
containerEle = document.getElementById('container'),
stage = new Konva.Stage({
container: "container",
size: {
width: containerEle.offsetWidth,
height: containerEle.offsetHeight
}
}),
layer = new Konva.Layer();
stage.add(layer);
const model = {
table: {
x: 100,
y: 100,
width: 200,
height: 400,
fill: 'black',
stroke: 'silver',
strokeWidth: 5
},
seat: {
radius: 40,
fill: 'white',
stroke: 'silver',
strokeWidth: 5,
gap: 20
},
seats: [{
name: "Seat 1",
x: "25%",
y: "-1r"
},
{
name: "Seat 2",
x: "75%",
y: "-1r"
},
{
name: "Seat 3",
tableX: 1,
tableY: 0,
x: "1r",
y: "16.6%"
},
{
name: "Seat 4",
tableX: 1,
tableY: 0,
x: "1r",
y: "50%"
},
{
name: "Seat 5",
tableX: 1,
tableY: 0,
x: "1r",
y: "83.3%"
},
{
name: "Seat 6",
tableX: 0,
tableY: 1,
x: "75%",
y: "1r"
},
{
name: "Seat 7",
tableX: 0,
tableY: 1,
x: "25%",
y: "1r"
},
]
}
// make the table
const table = new Konva.Rect(model.table);
layer.add(table)
for (const seat of model.seats) {
const seatShape = new Konva.Circle(model.seat);
let tablePos = {
x: seat.tableX && seat.tableX === 1 ? model.table.x + model.table.width : model.table.x,
y: seat.tableY && seat.tableY === 1 ? model.table.y + model.table.height : model.table.y
}
let position = {
x: tablePos.x + getPosComponent(seat.x, model.seat.radius, model.table.width, model.seat.gap),
y: tablePos.y + getPosComponent(seat.y, model.seat.radius, model.table.height, model.seat.gap)
}
seatShape.position(position)
layer.add(seatShape);
}
function getPosComponent(val, radius, size, gap) {
if (val.indexOf('r') > 0) {
let num = parseInt(val),
sign = Math.sign(num);
return sign * ((Math.abs(num) * radius) + gap);
} else if (val.indexOf('%') > 0) {
let num = parseFloat(val),
sign = Math.sign(num);
return sign * (size * num / 100);
}
throw new Error("Unexpected val format " + val);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset=UTF-8>
<script src="https://unpkg.com/konva#8/konva.min.js"></script>
<style>
#container {
width: 800px;
height: 600px;
}
</style>
</head>
<body>
<div id="container"></div>
</body>
</html>

React Native Wheel of Fortune

I am developing the react native application which contains the wheel of fortune. There are two wheels in my application. Wheel#1 is running smoothly but in Wheel#2 is being required to add Inner & Outer wheels. I am using D3 shape and React Native SVG packages to achieve my goal.
import React, {Component} from 'react';
import {
View,
StyleSheet,
Dimensions,
Animated,
TouchableOpacity,
Image,
} from 'react-native';
import * as d3Shape from 'd3-shape';
import Svg, {G, Text, TSpan, Path, Pattern} from 'react-native-svg';
const AnimatedSvg = Animated.createAnimatedComponent(Svg);
const {width, height} = Dimensions.get('screen');
class WheelOfFortune extends Component {
constructor(props) {
super(props);
this.state = {
enabled: false,
started: false,
finished: false,
winner: null,
gameScreen: new Animated.Value(width - 40),
wheelOpacity: new Animated.Value(1),
imageLeft: new Animated.Value(width / 2 - 30),
imageTop: new Animated.Value(height / 2 - 70),
};
this.angle = 0;
this.prepareWheel();
}
prepareWheel = () => {
this.Rewards = this.props.options.rewards;
this.RewardCount = this.Rewards.length;
this.numberOfSegments = this.RewardCount;
this.fontSize = 20;
this.oneTurn = 360;
this.angleBySegment = this.oneTurn / this.numberOfSegments;
this.angleOffset = this.angleBySegment / 2;
this.winner = this.props.options.winner
? this.props.options.winner
: Math.floor(Math.random() * this.numberOfSegments);
this._wheelPaths = this.makeWheel();
this._angle = new Animated.Value(0);
this.props.options.onRef(this);
};
resetWheelState = () => {
this.setState({
enabled: false,
started: false,
finished: false,
winner: null,
gameScreen: new Animated.Value(width - 40),
wheelOpacity: new Animated.Value(1),
imageLeft: new Animated.Value(width / 2 - 30),
imageTop: new Animated.Value(height / 2 - 70),
});
};
_tryAgain = () => {
this.prepareWheel();
this.resetWheelState();
this.angleListener();
this._onPress();
};
angleListener = () => {
this._angle.addListener(event => {
if (this.state.enabled) {
this.setState({
enabled: false,
finished: false,
});
}
this.angle = event.value;
});
};
componentWillUnmount() {
this.props.options.onRef(undefined);
}
componentDidMount() {
this.angleListener();
}
makeWheel = () => {
const data = Array.from({length: this.numberOfSegments}).fill(1);
const arcs = d3Shape.pie()(data);
var colors = this.props.options.colors
? this.props.options.colors
: [
'#E07026',
'#E8C22E',
'#ABC937',
'#4F991D',
'#22AFD3',
'#5858D0',
'#7B48C8',
'#D843B9',
'#E23B80',
'#D82B2B',
];
return arcs.map((arc, index) => {
const instance = d3Shape
.arc()
.padAngle(0.01)
.outerRadius(width / 2)
.innerRadius(this.props.options.innerRadius || 100);
return {
path: instance(arc),
color: colors[index % colors.length],
value: this.Rewards[index],
centroid: instance.centroid(arc),
};
});
};
_getWinnerIndex = () => {
const deg = Math.abs(Math.round(this.angle % this.oneTurn));
// wheel turning counterclockwise
if (this.angle < 0) {
return Math.floor(deg / this.angleBySegment);
}
// wheel turning clockwise
return (
(this.numberOfSegments - Math.floor(deg / this.angleBySegment)) %
this.numberOfSegments
);
};
_onPress = () => {
const duration = this.props.options.duration || 10000;
this.setState({
started: true,
});
Animated.timing(this._angle, {
toValue:
365 -
this.winner * (this.oneTurn / this.numberOfSegments) +
360 * (duration / 1000),
duration: duration,
useNativeDriver: true,
}).start(() => {
const winnerIndex = this._getWinnerIndex();
this.setState({
finished: true,
winner: this._wheelPaths[winnerIndex].value,
});
this.props.getWinner(this._wheelPaths[winnerIndex].value, winnerIndex);
});
};
_textRender = (x, y, number, i) => (
<Text
x={x - number.length * 5}
y={y - 80}
fill={
this.props.options.textColor ? this.props.options.textColor : '#fff'
}
textAnchor="middle"
fontSize={this.fontSize}>
{Array.from({length: number.length}).map((_, j) => {
// Render reward text vertically
if (this.props.options.textAngle === 'vertical') {
return (
<TSpan x={x} dy={this.fontSize} key={`arc-${i}-slice-${j}`}>
{number.charAt(j)}
</TSpan>
);
}
// Render reward text horizontally
else {
return (
<TSpan
y={y - 40}
dx={this.fontSize * 0.07}
key={`arc-${i}-slice-${j}`}>
{number.charAt(j)}
</TSpan>
);
}
})}
</Text>
);
_renderSvgWheel = () => {
return (
<View style={styles.container}>
{this._renderKnob()}
<Animated.View
style={{
alignItems: 'center',
justifyContent: 'center',
transform: [
{
rotate: this._angle.interpolate({
inputRange: [-this.oneTurn, 0, this.oneTurn],
outputRange: [
`-${this.oneTurn}deg`,
`0deg`,
`${this.oneTurn}deg`,
],
}),
},
],
backgroundColor: this.props.options.backgroundColor
? this.props.options.backgroundColor
: '#fff',
width: width - 20,
height: width - 20,
borderRadius: (width - 20) / 2,
borderWidth: this.props.options.borderWidth
? this.props.options.borderWidth
: 2,
borderColor: this.props.options.borderColor
? this.props.options.borderColor
: '#fff',
opacity: this.state.wheelOpacity,
}}>
<AnimatedSvg
width={this.state.gameScreen}
height={this.state.gameScreen}
viewBox={`0 0 ${width} ${width}`}
style={{
transform: [{rotate: `-${this.angleOffset}deg`}],
margin: 10,
}}>
<G y={width / 2} x={width / 2}>
{this._wheelPaths.map((arc, i) => {
const [x, y] = arc.centroid;
const number = arc.value.toString();
return (
<G key={`arc-${i}`}>
<Path d={arc.path} strokeWidth={2} fill={arc.color} />
<G
rotation={
(i * this.oneTurn) / this.numberOfSegments +
this.angleOffset
}
origin={`${x}, ${y}`}>
{this._textRender(x, y, number, i)}
</G>
</G>
);
})}
</G>
</AnimatedSvg>
</Animated.View>
</View>
);
};
_renderKnob = () => {
const knobSize = this.props.options.knobSize
? this.props.options.knobSize
: 20;
// [0, this.numberOfSegments]
const YOLO = Animated.modulo(
Animated.divide(
Animated.modulo(
Animated.subtract(this._angle, this.angleOffset),
this.oneTurn,
),
new Animated.Value(this.angleBySegment),
),
1,
);
return (
<Animated.View
style={{
width: knobSize,
height: knobSize * 2,
justifyContent: 'flex-end',
zIndex: 1,
opacity: this.state.wheelOpacity,
transform: [
{
rotate: YOLO.interpolate({
inputRange: [-1, -0.5, -0.0001, 0.0001, 0.5, 1],
outputRange: [
'0deg',
'0deg',
'35deg',
'-35deg',
'0deg',
'0deg',
],
}),
},
],
}}>
<Svg
width={knobSize}
height={(knobSize * 100) / 57}
viewBox={`0 0 57 100`}
style={{
transform: [{translateY: 8}],
}}>
<Image
source={
this.props.options.knobSource
? this.props.options.knobSource
: require('../assets/images/knob.png')
}
style={{ width: knobSize, height: (knobSize * 100) / 57 }}
/>
</Svg>
</Animated.View>
);
};
_renderTopToPlay() {
if (this.state.started == false) {
return (
<TouchableOpacity onPress={() => this._onPress()}>
{this.props.options.playButton()}
</TouchableOpacity>
);
}
}
render() {
return (
<View style={styles.container}>
<TouchableOpacity
style={{
position: 'absolute',
width: width,
height: height / 2,
justifyContent: 'center',
alignItems: 'center',
}}>
<Animated.View style={[styles.content, {padding: 10}]}>
{this._renderSvgWheel()}
</Animated.View>
</TouchableOpacity>
{this.props.options.playButton ? this._renderTopToPlay() : null}
</View>
);
}
}
export default WheelOfFortune;
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
content: {},
startText: {
fontSize: 50,
color: '#fff',
fontWeight: 'bold',
textShadowColor: 'rgba(0, 0, 0, 0.4)',
textShadowOffset: {width: -1, height: 1},
textShadowRadius: 10,
},
});
End Result Look like this
Now I want to add an Inner wheel to it Like this

stacked barchart with svg (no 3rd party library)

I am trying to create a stacked barchart with svg and html and not using any 3rd party library. Unfortunately, there is not a single document online which shows how to create a stacked bar chart using plain svg.
I have created a codepen and i am midway to achieving that stacked barchart. Can anyone please let me know what else is needed to make this a stacked barchart.
https://codepen.io/a166617/pen/qBXvzQd
Here is the code that i currently have
const ReleaseScopeCharts = () => {
const data = [
{
name: 'Transit',
passed: 2,
skipped: 5,
failed: 22,
},
{
name: 'Access',
passed: 7,
skipped: 2,
failed: 11,
},
];
const width = 500;
const colors = ['#30D158', '#005EA7', '#FF453A'];
const entries = data.map((d) => ({
name: d.name,
total: ['passed', 'skipped', 'failed'].reduce((acc, key) => acc + d[key], 0),
bars: ['passed', 'skipped', 'failed'].map((key, i) => ({
value: d[key],
color: colors[i],
}))
.filter((bar) => bar.value),
}));
const rows = (entry) => entry.bars.map((bar, index) => {
const height = (bar.value / entry.total) * 100;
return (
<g key={index}>
<rect
width={50}
height={`${height}%`}
fill={bar.color}
x={index * 60} // multiply with the width (50) + 10 for space
/>
</g>
);
});
return (
<div className="new-card">
<div />
{entries.map((entry) => (
<>
{entry.name}
<svg viewBox={`0, 0, ${width}, ${500}`}
height={500}
width={width}
style={{ transform: `rotateX(180deg)` }}
>
{rows(entry)}
</svg>
</>
))}
</div>
);
};
With stacked barchart, i mean showing one over the other.
To stack barchart, you need to calculate the current columns and space widths. Wrap the svg to div, also offset the text in to div and centered with display:flex.
Add the y key to the bars, where:
start point = passed = 0
middle point = skipped = passed value
end point = failed = passed value + skipped value
y: key === 'passed' ? 0 : key === 'skipped' ? d['passed'] : d['skipped'] + d['passed'],
// Basic style
const newCardStyle = {
display: 'flex',
};
const contentStyle = {
display: 'flex',
flexFlow: 'column',
alignItems: 'center',
};
// multiply 50 (width) * 3 (columns) + 10 (space width) * 2 ( space between columns)
const width = 50 * 3 + 10 * 3;
function App() {
const data = [
{
name: 'Transit',
passed: 2,
skipped: 5,
failed: 22,
},
{
name: 'Access',
passed: 7,
skipped: 2,
failed: 11,
},
];
// Basic style
const newCardStyle = {
display: 'flex',
};
const contentStyle = {
display: 'flex',
flexFlow: 'column',
alignItems: 'center',
};
// multiply 50 (width) * 3 (columns) + 10 (space width) * 2 ( space between columns)
const width = 50 * 3 + 10 * 3;
const colors = ['#30D158', '#005EA7', '#FF453A'];
const entries = data.map(d => ({
name: d.name,
total: ['passed', 'skipped', 'failed'].reduce(
(acc, key) => acc + d[key],
0
),
bars: ['passed', 'skipped', 'failed']
.map((key, i) => ({
value: d[key],
color: colors[i],
y:
key === 'passed'
? 0
: key === 'skipped'
? d['passed']
: d['skipped'] + d['passed'],
}))
.filter(bar => bar.value),
}));
const rows = entry => {
return entry.bars.map((bar, index) => {
const height = (bar.value / entry.total) * 100;
const y = (bar.y / entry.total) * 100;
return (
<g key={Math.random()}>
<rect
width={50}
height={`${height}%`}
fill={bar.color}
x={60} // multiply with the width (50) + 10 for space
y={`${y}%`}
/>
</g>
);
});
};
return (
<div className="new-card" style={newCardStyle}>
{entries.map(entry => (
<div style={contentStyle} key={Math.random()}>
<svg
viewBox={`0, 0, ${width}, ${500}`}
height={500}
width={width}
style={{ transform: `rotateX(180deg)` }}
>
{rows(entry)}
</svg>
{entry.name}
</div>
))}
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://unpkg.com/react#17/umd/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js" crossorigin></script>
<div id="root"></div>

Panresponder or Animated.View doesnt work when the item to animate is in front of a scrollable view

Hey everyone :) Should be a function for a navigation avatar which sticks to the closest corner. While coding I used a simple circle as a placeholder. The problem is that the following code works perfectly fine when imported to another screen, but when I replace <View style={styles.circle} /> with an image <Image source={require("../assets/dude.png")} resizeMode="contain" style={{width: 180, height: 240,}}/> it doesnt work anymore? Like I can see the image, but the animations work extremely buggy and it just goes anywhere, nothing like it's supposed to do?
I tried it also with Animated.Image instead of the view and giving it all the parameters, still no change. The weird thing is that the Image works perfectly fine if I were to run this code as a screen itself, but when I import it only the circle view works, not the image?
EDIT: Just found the issue: if the Animated.Image is in front of a Scrollable View, even if it isn't part of that View, it bugs. If I replace the image with anything else (like a Box), it works fine, only the image bugs in that manner :) which leads me to my next question: How can I fix that?
so this is my code:
import React from "react";
import {
StyleSheet,
View,
Dimensions,
Animated,
PanResponder,
Image,
} from "react-native";
export default class Avatar extends React.Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
screenMeasurements: {
width: Screen.width / 2,
height: Screen.height / 2,
},
};
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
this.state.pan.setOffset({
x: this.state.pan.x._value,
y: this.state.pan.y._value,
});
},
onPanResponderMove: Animated.event([
null,
{
dx: this.state.pan.x,
dy: this.state.pan.y,
},
]),
onPanResponderRelease: (e, gesture) => {
if (this.whichField(gesture) == 1) {
console.log("top left");
this.state.pan.flattenOffset();
Animated.spring(this.state.pan, {
toValue: {
x: (Screen.width * 0.5 - 90) * -1,
y: (Screen.height * 0.5 - 120) * -1,
},
}).start();
} else if (this.whichField(gesture) == 2) {
console.log("top right");
this.state.pan.flattenOffset();
Animated.spring(this.state.pan, {
toValue: {
x: Screen.width * 0.5 - 90,
y: (Screen.height * 0.5 - 120) * -1,
},
}).start();
} else if (this.whichField(gesture) == 3) {
console.log("bottom left");
this.state.pan.flattenOffset();
Animated.spring(this.state.pan, {
toValue: {
x: (Screen.width * 0.5 - 90) * -1,
y: Screen.height * 0.5 - 150,
},
}).start();
} else {
console.log("bottom right");
this.state.pan.flattenOffset();
Animated.spring(this.state.pan, {
toValue: {
x: Screen.width * 0.5 - 90,
y: Screen.height * 0.5 - 150,
},
}).start();
}
},
});
}
whichField(gesture) {
var sm = this.state.screenMeasurements;
let field;
{
gesture.moveY < sm.height && gesture.moveX < sm.width
? (field = 1)
: gesture.moveY < sm.height && gesture.moveX > sm.width
? (field = 2)
: gesture.moveY > sm.height && gesture.moveX < sm.width
? (field = 3)
: (field = 4);
}
return field;
}
render() {
return (
<View style={styles.draggableContainer}>
<Animated.View
style={[this.state.pan.getLayout()]}
{...this.panResponder.panHandlers}
>
<View style={styles.circle} />
</Animated.View>
</View>
);
}
}
let Screen = Dimensions.get("screen");
let CIRCLE_RADIUS = 45;
const styles = StyleSheet.create({
text: {
marginTop: 25,
marginLeft: 5,
marginRight: 5,
textAlign: "center",
color: "#fff",
},
draggableContainer: {
position: "absolute",
top: Screen.height / 2 - CIRCLE_RADIUS,
left: Screen.width / 2 - CIRCLE_RADIUS,
},
circle: {
backgroundColor: "#1abc9c",
width: CIRCLE_RADIUS * 2,
height: CIRCLE_RADIUS * 2,
borderRadius: CIRCLE_RADIUS,
},
});
Check out this similar animation of badge Above ScrollView.
You need to place the Image inside an Animated View.
Example code:
import React, {Component} from 'react';
import {
StyleSheet,
View,
PanResponder,
Animated,
Dimensions,
ScrollView,
Image,
} from 'react-native';
const {height, width} = Dimensions.get('screen');
export default class SampleApp extends Component {
constructor() {
super();
this._animatedValue = new Animated.ValueXY({x: 20, y: 20});
this._value = {x: 20, y: 20};
this._animatedValue.addListener((value) => (this._value = value));
this._panResponder = PanResponder.create({
onMoveShouldSetResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: (e, gestureState) => {
this._animatedValue.setOffset({x: this._value.x, y: this._value.y});
this._animatedValue.setValue({x: 0, y: 0});
},
onPanResponderMove: Animated.event([
null,
{dx: this._animatedValue.x, dy: this._animatedValue.y},
]),
onPanResponderRelease: (e, gesture) => {
this._animatedValue.flattenOffset();
if (this.whichField(gesture) == 1) {
Animated.spring(this._animatedValue, {
toValue: {x: 20, y: 20},
}).start();
} else if (this.whichField(gesture) == 2) {
Animated.spring(this._animatedValue, {
toValue: {x: width - 120, y: 20},
}).start();
} else if (this.whichField(gesture) == 3) {
Animated.spring(this._animatedValue, {
toValue: {x: 20, y: height - 150},
}).start();
} else {
Animated.spring(this._animatedValue, {
toValue: {x: width - 120, y: height - 150},
}).start();
}
},
});
}
whichField(gesture) {
var sm = {height, width};
let field;
{
gesture.moveY < sm.height / 2 && gesture.moveX < sm.width / 2
? (field = 1)
: gesture.moveY < sm.height / 2 && gesture.moveX > sm.width / 2
? (field = 2)
: gesture.moveY > sm.height / 2 && gesture.moveX < sm.width / 2
? (field = 3)
: (field = 4);
}
return field;
}
render() {
return (
<View style={styles.container}>
<ScrollView>
{['', '', '', '', '', ''].map(() => (
<View style={styles.scrollItem} />
))}
</ScrollView>
<Animated.View
style={[
styles.box,
{
transform: [
{translateX: this._animatedValue.x},
{translateY: this._animatedValue.y},
],
},
]}
{...this._panResponder.panHandlers}>
<Image
source={{
uri:
'https://pluspng.com/img-png/user-png-icon-male-user-icon-512.png',
}}
style={StyleSheet.absoluteFill}
/>
</Animated.View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
box: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: '#fff',
position: 'absolute',
},
scrollItem: {
height: 300,
width: '100%',
backgroundColor: 'grey',
marginBottom: 10,
},
});
Check in Expo
I hope it will help you.

Adding Labels to the Left or Right of Highcharts Funnel Visualization

My current visualization is as follows:
$(function() {
var dataEx = [
['1 Visit', 352000],
['2 Visits', 88000],
['3+ Visits', 42000]
],
len = dataEx.length,
sum = 0,
minHeight = 0.05,
data = [];
//specify your percent of prior visit value manually here:
var perc = [100, 25, 48];
for (var i = 0; i < len; i++) {
sum += dataEx[i][1];
}
for (var i = 0; i < len; i++) {
var t = dataEx[i],
r = t[1] / sum;
data[i] = {
name: t[0],
y: (r > minHeight ? t[1] : sum * minHeight),
percent: perc[i], // <----- this here is manual input
//percent: Math.round(r * 100), <--- this here is mathematical
label: t[1]
}
}
console.log(dataEx, data)
$('#container').highcharts({
chart: {
type: 'funnel',
marginRight: 100,
events: {
load: function() {
var chart = this;
Highcharts.each(chart.series[0].data, function(p, i) {
var bBox = p.dataLabel.getBBox()
p.dataLabel.attr({
x: (chart.plotWidth - chart.plotLeft) / 2,
'text-anchor': 'middle',
y: p.labelPos.y - (bBox.height / 2)
})
})
},
redraw: function() {
var chart = this;
Highcharts.each(chart.series[0].data, function(p, i) {
p.dataLabel.attr({
x: (chart.plotWidth - chart.plotLeft) / 2,
'text-anchor': 'middle',
y: p.labelPos.y - (bBox.height / 2)
})
})
}
},
},
title: {
text: 'Guest Return Funnel',
x: -50
},
tooltip: {
//enabled: false
formatter: function() {
return '<b>' + this.key +
'</b><br/>Percent of Prior Visit: '+ this.point.percent + '%<br/>Guests: ' + Highcharts.numberFormat(this.point.label, 0);
}
},
plotOptions: {
series: {
allowPointSelect: true,
borderWidth: 12,
animation: {
duration: 400
},
dataLabels: {
enabled: true,
connectorWidth: 0,
distance: 0,
formatter: function() {
var point = this.point;
console.log(point);
return '<b>' + point.name + '</b> (' + Highcharts.numberFormat(point.label, 0) + ')<br/>' + point.percent + '%';
},
minSize: '10%',
color: 'black',
softConnector: true
},
neckWidth: '30%',
neckHeight: '0%',
width: '50%',
height: '110%'
//old options are as follows:
//neckWidth: '50%',
//neckHeight: '50%',
//-- Other available options
//height: '200'
// width: pixels or percent
}
},
legend: {
enabled: false
},
series: [{
name: 'Unique users',
data: data
}]
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="http://code.highcharts.com/highcharts.js"></script>
<script src="http://code.highcharts.com/modules/funnel.js"></script>
<script src="http://code.highcharts.com/modules/exporting.js"></script>
<div id="container" style="width: 500px; height: 400px; margin: 0 auto"></div>
I would like to do the following (a photo that clarifies what I would like is HERE):
Put the category names "1 Visit", "2 Visits", "3 Visits" to the LEFT of
the funnel.
Arrange the number of guest amount and percent for each category of the
funnel so that it appears like (INSIDE the funnel):
352K 100%
Right now I have the values as the full number like 352000 but I'm
wondering if there's a way to make all numbers with 000 at the end into
a "K" at the end.
It would be great if I could also add labels for those two values AT THE
TOP of the funnel ("Guests" and "Percent of Prior Visit").
Add 2 more labels to the RIGHT of the funnel called "Q1/17 TTM" and "Avg
Value" and have values be placed for each category of the funnel
below the labels. The values for "Q1/17 TTM" should be red and the
values for "Avg Value" should be gray.
The values for "Q1/17 TTM" begin at the "2 Visits" and end at the very
bottom (under the last category)
Values for "Avg Value" begin at the first category and end at the last
category.
At the very bottom of the visualization, have a value. Don't worry about
what this is (and this is the value $12.9M in the photo).
And I want these changes to still make the data processing algorithm to visualize small values work. I would really appreciate the help! Thank you.
There isn not any option in Highcharts to handle more than one datalabels, but you can use text SVGRenderer to add additional text elements, for example:
events: {
render: function() {
var chart = this;
Highcharts.each(chart.series[0].data, function(p, i) {
var bBox = p.dataLabel.getBBox();
p.dataLabel.attr({
x: (chart.plotWidth - chart.plotLeft) / 2,
'text-anchor': 'middle',
y: p.labelPos.y - (bBox.height / 2)
});
if (p.dataLabel1) {
p.dataLabel1.destroy();
p.dataLabel2.destroy();
}
p.dataLabel1 = chart.renderer.text(p.name, p.dataLabel.x - 150, p.dataLabel.y + chart.plotTop - bBox.y).add();
p.dataLabel2 = chart.renderer.text('some Text', p.dataLabel.x + 150, p.dataLabel.y + chart.plotTop - bBox.y).add();
});
}
}
Live demo: https://jsfiddle.net/BlackLabel/w2cs4ufe/1/
API Reference: https://api.highcharts.com/class-reference/Highcharts.SVGRenderer#text

Categories

Resources