I'm creating a component in my application built with Vue. This componet is a countdown, ranging from X minutes to 00:00.
I know it's possible to animate svg to achieve the desired result, but I do not have the necessary knowledge. I have never used any svg library.
I need to create the following animation in my progress component:
The animation need to follow the path according to the weather in a smooth way. The path nodes should be inserted / updated based on time.
This is my actual countdown component:
var app = new Vue({
el: '#app',
data: {
date: moment(2 * 60 * 1000)
},
computed: {
time: function(){
return this.date.format('mm:ss');
}
},
mounted: function(){
var timer = setInterval(() => {
this.date = moment(this.date.subtract(1, 'seconds'));
if(this.date.diff(moment(0)) === 0){
clearInterval(timer);
alert('Done!');
}
}, 1000);
}
});
<script src="https://momentjs.com/downloads/moment.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">{{ time }}</div>
This is the svg for the progress circle:
<svg x="0px" y="0px" viewBox="0 0 90 90">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:none;stroke:#B5B5B5;stroke-miterlimit:10;}
.st2{fill:none;stroke:#408EFF;stroke-linecap:round;stroke-miterlimit:10;}
.st3{fill:#408EFF;}
</style>
<rect class="st0" width="90" height="90"/>
<circle class="st1" cx="45" cy="45" r="40"/>
<path class="st2" d="M45,5c22.1,0,40,17.9,40,40S67.1,85,45,85S5,67.1,5,45S22.9,5,45,5"/>
<circle class="st3" cx="45" cy="5" r="3"/>
</svg>
How can I achieve the desired result?
All help would be welcome.
You'll need to familiarize yourself with SVG shapes, in particular <path> in order to make the arc.
Here's an example:
Vue.component('progress-ring', {
template: '#progress-ring',
props: {
value: {
type: Number,
default: 0,
},
min: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 1,
},
text: {
type: null,
default: '',
},
},
computed: {
theta() {
const frac = (this.value - this.min) / (this.max - this.min) || 0;
return frac * 2 * Math.PI;
},
path() {
const large = this.theta > Math.PI;
return `M0,-46 A46,46,0,${large ? 1 : 0},1,${this.endX},${this.endY}`;
},
endX() {
return Math.cos(this.theta - Math.PI * 0.5) * 46;
},
endY() {
return Math.sin(this.theta - Math.PI * 0.5) * 46;
},
},
});
new Vue({
el: '#app',
});
body {
font-family: sans-serif;
}
.progress-ring {
width: 100px;
height: 100px;
}
.progress-ring-circle {
stroke: rgba(0, 0, 0, 0.1);
stroke-width: 1;
fill: none;
}
.progress-ring-ring {
stroke: #007fff;
stroke-width: 2;
fill: none;
}
.progress-ring-end {
fill: #007fff;
}
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<div id="app">
<progress-ring :min="0" :max="100" :value="40" text="12:34"></progress-ring>
</div>
<template id="progress-ring">
<svg class="progress-ring" viewBox="-50,-50,100,100">
<circle class="progress-ring-circle" r="46"/>
<path class="progress-ring-ring" :d="path"/>
<circle class="progress-ring-end" :cx="endX" :cy="endY" r="4"/>
<text alignment-baseline="middle" text-anchor="middle">{{ text }}</text>
</svg>
</template>
As for animating it, you just need to use JavaScript to change the value prop by using, for example, setInterval or some other means.
Follow your template, one solution is pre-define the path into one array (each path node is one element of the array). Then push the path node to current progress path for each interval.
Like below demo:
var app = new Vue({
el: '#app',
data: {
date: moment(2 * 60 * 1000),
pathRoute: ['M45 5', 'c22.1 0 40 17.9 40 40','S67.1 85 45 85','S5 67.1 5 45','S22.9 5 45 5'],
pathProgess: [],
stepIndex: 0
},
computed: {
time: function(){
return this.date.format('mm:ss');
},
computedProgress: function () {
return this.pathProgess.join(' ')
}
},
mounted: function(){
var timer = setInterval(() => {
this.date = moment(this.date.subtract(1, 'seconds'));
this.$set(this.pathProgess, this.stepIndex, this.pathRoute[this.stepIndex])
this.stepIndex++
if(this.date.diff(moment(0)) === 0){
clearInterval(timer);
}
}, 1000);
}
});
.st0{fill:#FFFFFF;}
.st1{fill:none;stroke:#B5B5B5;stroke-miterlimit:10;}
.st2{fill:none;stroke:#408EFF;stroke-linecap:round;stroke-miterlimit:10;}
.st3{fill:#408EFF;}
<script src="https://momentjs.com/downloads/moment.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<p>{{computedProgress}}</p>
<svg x="0px" y="0px" viewBox="0 0 90 90">
<rect class="st0" width="90" height="90"/>
<circle class="st1" cx="45" cy="45" r="40"/>
<text class="circle-chart-percent" x="20.91549431" y="40.5" font-size="8">{{time}}</text>
<path class="st2" :d="computedProgress"/>
<circle class="st3" cx="45" cy="5" r="3"/>
</svg>
</div>
Or you can use the approach Answered at another question, to real time calculate the path.
var app = new Vue({
el: '#app',
data: {
date: moment(2 * 60 * 1000),
pathProgess: ''
},
computed: {
time: function(){
return this.date.format('mm:ss');
}
},
mounted: function(){
let maxValue = this.date.diff(moment(0), 'seconds') //total seconds
var timer = setInterval(() => {
this.date = moment(this.date.subtract(1, 'seconds'))
let curValue = this.date.diff(moment(0), 'seconds') // current seconds
this.pathProgess = this.describeArc(45, 45, 40, 0, (maxValue-curValue)*360/maxValue)
if(this.date.diff(moment(0)) === 0){
clearInterval(timer);
}
}, 1000);
},
methods: {
//copy from https://stackoverflow.com/a/18473154/5665870
polarToCartesian: function (centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
},
//copy from https://stackoverflow.com/a/18473154/5665870
describeArc: function (x, y, radius, startAngle, endAngle){
var start = this.polarToCartesian(x, y, radius, endAngle);
var end = this.polarToCartesian(x, y, radius, startAngle);
var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
].join(" ");
return d;
}
}
});
.st0{fill:#FFFFFF;}
.st1{fill:none;stroke:#B5B5B5;stroke-miterlimit:10;}
.st2{fill:none;stroke:#408EFF;stroke-linecap:round;stroke-miterlimit:10;}
.st3{fill:#408EFF;}
<script src="https://momentjs.com/downloads/moment.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<p>{{pathProgess}}</p>
<svg x="0px" y="0px" viewBox="0 0 90 90">
<rect class="st0" width="90" height="90"/>
<circle class="st1" cx="45" cy="45" r="40"/>
<text class="circle-chart-percent" x="20.91549431" y="40.5" font-size="8">{{time}}</text>
<path class="st2" :d="pathProgess"/>
<circle class="st3" cx="45" cy="5" r="3"/>
</svg>
</div>
Related
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?
I have the following SVG:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="900" height="800">
<path d="M 650,750 L 50,750 L 100,350 L 850,50 L 850,550 L 650,750" fill="none" stroke="black" stroke-width="3" />
</svg>
When I try to render it with ExtrudeGeometry, I get the following:
The filling is set to none, so it should only have borders (at least this is what I am trying to achieve).
This is what I'm using to load it:
const svgResult = loader.parse(svgTxt);
svgResult.paths.forEach((path) => {
const shapes = SVGLoader.createShapes(path);
shapes.forEach((shape) => {
const geom = new THREE.ExtrudeGeometry(shape, {
bevelEnabled: true,
bevelSize: 14,
bevelThickness: 5,
bevelSegments: 15,
depth: 20,
});
const mesh = new THREE.Mesh(
geom,
new THREE.MeshPhysicalMaterial({
color: "white",
})
);
...
}
}
I have such an element made in SVG:
import Svg, { Path, Circle } from "react-native-svg"
function CloseIcon(props) {
return (
<Svg
viewBox="0 0 1000 1000"
fillRule="evenodd"
clipRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit={2}
style={styles.shadow}
>
<Path
d="M500 420.886L781.42 139.464c19.297-19.28 50.622-19.28 69.919 0l9.179 9.195c19.296 19.28 19.296 50.623 0 69.903L579.097 499.983l281.438 281.455c19.296 19.28 19.296 50.622 0 69.902l-9.179 9.195c-19.296 19.28-50.622 19.28-69.918 0L500 579.081 218.562 860.535c-19.297 19.28-50.623 19.28-69.919 0l-9.179-9.195c-19.296-19.28-19.296-50.622 0-69.902l281.438-281.455-281.421-281.421c-19.297-19.28-19.297-50.623 0-69.903l9.178-9.195c19.297-19.28 50.623-19.28 69.92 0l281.42 281.422z"
fill="#fff"
/>
</Svg>
)
};
Is it possible to add a shadow to it?
These types of options do not work: (this solution treats the path as a rectangle)
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.23,
shadowRadius: 2.62,
elevation: 4,
how to display an icon by type attr in vue.js?
HTML
<icon type="heart"></icon>
<icon type="heartFull"></icon>
Vue
Vue.component('icon', {
data: {
type: {
heart: '<g stroke-linecap="round" stroke-linejoin="round" stroke-width="2" fill="#c3cad5" stroke="#c3cad5"><path fill="none" stroke="#c3cad5" stroke-miterlimit="10" d="M21.243,3.757 c-2.343-2.343-6.142-2.343-8.485,0c-0.289,0.289-0.54,0.6-0.757,0.927c-0.217-0.327-0.469-0.639-0.757-0.927 c-2.343-2.343-6.142-2.343-8.485,0c-2.343,2.343-2.343,6.142,0,8.485L12,21.485l9.243-9.243C23.586,9.899,23.586,6.1,21.243,3.757z"></path></g>',
heartFull: '<g fill="#c3cad5"><path fill="#c3cad5" d="M21.95,3.051C20.627,1.729,18.87,1,17,1s-3.627,0.729-4.949,2.05C12.034,3.067,12.017,3.084,12,3.102 c-0.017-0.018-0.033-0.034-0.05-0.051C10.627,1.729,8.87,1,7,1S3.373,1.729,2.05,3.051S0,6.13,0,8s0.728,3.627,2.05,4.949l9.95,9.95 l9.95-9.95C23.272,11.627,24,9.87,24,8C24,6.131,23.272,4.373,21.95,3.051z"></path></g>'
}
},
props: {
width: {
type: Number,
default: 24
},
height: {
type: Number,
default: 24
},
},
computed: {
viewBox() {
return '0 0 ' + this.width + ' ' + this.height
}
},
template: '<svg xmlns="http://www.w3.org/2000/svg" :viewBox="viewBox" :width="width" :height="height">{{ type }}</svg>',
})
Expected result
<icon type="heart"></icon>
it turns into
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24" width="24" height="24"><g stroke-linecap="round" stroke-linejoin="round" stroke-width="2" fill="#c3cad5" stroke="#c3cad5"><path fill="none" stroke="#c3cad5" stroke-miterlimit="10" d="M21.243,3.757 c-2.343-2.343-6.142-2.343-8.485,0c-0.289,0.289-0.54,0.6-0.757,0.927c-0.217-0.327-0.469-0.639-0.757-0.927 c-2.343-2.343-6.142-2.343-8.485,0c-2.343,2.343-2.343,6.142,0,8.485L12,21.485l9.243-9.243C23.586,9.899,23.586,6.1,21.243,3.757z"></path></g></svg>
or
<icon type="heartFull"></icon>
it turns into
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24" width="24" height="24"><g fill="#c3cad5"><path fill="#c3cad5" d="M21.95,3.051C20.627,1.729,18.87,1,17,1s-3.627,0.729-4.949,2.05C12.034,3.067,12.017,3.084,12,3.102 c-0.017-0.018-0.033-0.034-0.05-0.051C10.627,1.729,8.87,1,7,1S3.373,1.729,2.05,3.051S0,6.13,0,8s0.728,3.627,2.05,4.949l9.95,9.95 l9.95-9.95C23.272,11.627,24,9.87,24,8C24,6.131,23.272,4.373,21.95,3.051z"></path></g></svg>
In this way I would create more icons over time and use them easily. Thanks!
Well your type is not a prop but a data property. And it is an object. So right now you are inserting the whole object type from your data into svg.
Try this:
Vue.component('icon', {
props: {
iconType: {
type: String,
default: "heart"
},
width: {
type: Number,
default: 24
},
height: {
type: Number,
default: 24
}
},
data() {
return {
types: {
heart:
'<g stroke-linecap="round" stroke-linejoin="round" stroke-width="2" fill="#c3cad5" stroke="#c3cad5"><path fill="none" stroke="#c3cad5" stroke-miterlimit="10" d="M21.243,3.757 c-2.343-2.343-6.142-2.343-8.485,0c-0.289,0.289-0.54,0.6-0.757,0.927c-0.217-0.327-0.469-0.639-0.757-0.927 c-2.343-2.343-6.142-2.343-8.485,0c-2.343,2.343-2.343,6.142,0,8.485L12,21.485l9.243-9.243C23.586,9.899,23.586,6.1,21.243,3.757z"></path></g>',
heartFull:
'<g fill="#c3cad5"><path fill="#c3cad5" d="M21.95,3.051C20.627,1.729,18.87,1,17,1s-3.627,0.729-4.949,2.05C12.034,3.067,12.017,3.084,12,3.102 c-0.017-0.018-0.033-0.034-0.05-0.051C10.627,1.729,8.87,1,7,1S3.373,1.729,2.05,3.051S0,6.13,0,8s0.728,3.627,2.05,4.949l9.95,9.95 l9.95-9.95C23.272,11.627,24,9.87,24,8C24,6.131,23.272,4.373,21.95,3.051z"></path></g>'
}
};
},
computed: {
viewBox() {
return "0 0 " + this.width + " " + this.height;
}
},
template:
'<svg xmlns="http://www.w3.org/2000/svg" :viewBox="viewBox" :width="width" :height="height" v-html="this.types[this.iconType]"></svg>'
};
})
<icon iconType ="heart"></icon>
<icon iconType ="heartFull"></icon>
This is the code generated by highcharts for lines and point. I'm using inspect element and points are where they should be but they seem to be transparent or something like behind he chart. but the code looks right.
<g class="highcharts-series-group" zIndex="3">
<g class="highcharts-series" clip-path="url(http://localhost:63540/admin/reports.aspx?type=like#highcharts-1)" visibility="visible" transform="translate(40,10)">
<path d="M 244 50.4 L 732 302.6" fill="none" stroke="rgb(0, 0, 0)" stroke-width="5" isShadow="true" stroke-opacity="0.05" transform="translate(1,1)"></path>
<path d="M 244 50.4 L 732 302.6" fill="none" stroke="rgb(0, 0, 0)" stroke-width="3" isShadow="true" stroke-opacity="0.1" transform="translate(1,1)"></path>
<path d="M 244 50.4 L 732 302.6" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1" isShadow="true" stroke-opacity="0.15000000000000002" transform="translate(1,1)"></path>
<path d="M 244 50.4 L 732 302.6" fill="none" stroke="#4D4D4D" stroke-width="2"></path><path d="M 732 298.6 C 737.328 298.6 737.328 306.6 732 306.6 C 726.672 306.6 726.672 298.6 732 298.6 Z" fill="#4D4D4D" stroke="#FFFFFF" stroke-width="0.000001" zIndex="2000"></path>
<path d="M 244 46.4 C 249.328 46.4 249.328 54.4 244 54.4 C 238.672 54.4 238.672 46.4 244 46.4 Z" fill="#4D4D4D" stroke="#FFFFFF" stroke-width="0.000001"></path></g></g>
What can be wrong with it?
It's funny that I have copied the code from my other project (that is working) and only have changed the data.
this is the compiled js code:
var dates = ['1394/12/06','1394/12/11'];
after.push(function () {
chart = new Highcharts.Chart({
chart: {
height: 500,
backgroundColor:'#eee',
renderTo: 'chart',
defaultSeriesType: 'line',
// marginRight: 130,
//marginBottom: 150,
// marginTop: 20,
// events: { click: function () { startWait($('html')); document.location = 'price.aspx' } }
},
title: {
text: '',
x: -20 //center
},
subtitle: {
text: '',
x: -20
},
xAxis: {
categories: dates,
labels: {
rotation: 90,
formatter: function () {
return this.value;
},
y:40
}
},
yAxis: {
title: {
text: ''
},
plotLines: [{
value: 0,
width: 1,
color: '#808080'
}],
labels: {
formatter: function () {
// return toFarsi(virgulize(this.value)) ;
}
}
},
tooltip: {
formatter: function () {
return "<b>" + this.y + "</b>";
}
},
legend: {
//enabled: false
},
series: [
{
name:' likes',
color:'#4D4D4D',
data: [2,1]
},
]
});
});
Well after disabling css, javascript, and many other tests nothing worked.
Thanks to jlbriggs his fiddle gave me the idea of testing the code with older version of jquery.
It worked with older version of jquery.
Update:
New version of Highcharts works with the latest version of jquery.