Complex SVG animation in ReactJS - javascript
I am using React to build an animated intro that comprises three taglines paired with animated SVG icons.
I opted for TweenMax to manage the SVG animations, due to the solid cross-browser support it offers. This also would allow me to perform a simple morph of the d attribute on <path> elements. I am combining the aforementioned with ReactTransitionGroup
However, I ran into the following problems while trying to make TweenMax and React play nice:
While opting for component state, componentWillReceiveProps is somehow called twice in console. This would mean, my animation method would be called twice. What is causing this and might storing the tag index using Redux be a better option in this use case?
The way that I am animating currently seems very crude, e.g. repeating this.refs and findDOMNode() repeatedly. Surely there must be a better way? In which ways could the structure of my code be improved?
I am completely stumped and would love to be pointed in the right direction.
Kind regards, Jason
Code
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
body {
background: rgb(240, 90, 48);
font: bold 1em sans-serif;
color: rgb(255, 255, 255);
text-align: center;
}
.reveal-wrap {
position: absolute;
width: 200px;
height: 200px;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
overflow: hidden;
}
.reveal-icon {
width: 100px;
height: 100px;
margin: 0 auto 2em auto;
}
.reveal-icon svg {
width: 100%;
height: 100%;
}
.reveal-icon .fill {
fill: rgb(251, 163, 10);
}
.reveal-icon .mask {
fill: rgb(240, 90, 48);
}
.reveal-icon .stroke {
stroke: rgb(251, 163, 10);
stroke-linecap: round;
stroke-width: 5;
}
.reveal-text {
position: absolute;
width: 100%;
}
.switch-locale {
position: fixed;
top: 1em;
left: 1em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.25/browser.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/gsap/latest/TweenMax.min.js"></script>
<script src="https://fb.me/react-with-addons-0.13.3.min.js"></script>
<div id="react-root"></div>
<script type="text/babel">
const ReactTransitionGroup = React.addons.TransitionGroup
const UI_TEXT = {
EN_US: {
REVEAL: [
{ ID: 0, TEXT: 'Tagline 1' },
{ ID: 1, TEXT: 'Tagline 2' },
{ ID: 2, TEXT: 'Tagline 3' }
]
},
NL_NL: {
REVEAL: [
{ ID: 0, TEXT: 'Slagzin 1' },
{ ID: 1, TEXT: 'Slagzin 2' },
{ ID: 2, TEXT: 'Slagzin 3' }
]
}
}
class Reveal extends React.Component {
constructor() {
super();
this.state = {
index: 0,
locale: 'EN_US'
}
}
nextTagline() {
this.setState({index: this.state.index + 1})
console.log('this.state.index # ' + this.state.index)
}
switchLocale() {
let locale = (this.state.locale === 'EN_US') ? 'NL_NL' : 'EN_US'
this.setState({locale})
}
render() {
return (
<ReactTransitionGroup className='reveal-wrap' component='div'>
<RevealIcon tag={this.state.index} nextTag={() => this.nextTagline()} />
<RevealText tag={this.state.index} locale={this.state.locale} key={this.state.index} />
<SwitchLocale switchLocale={() => this.switchLocale()} />
</ReactTransitionGroup>
)
}
}
class RevealText extends React.Component {
fadeIn(callback, delay) {
TweenLite.fromTo(React.findDOMNode(this), 0.5,
{
y: '100px',
opacity: 0
},
{
y: 0,
delay: delay,
opacity: 1,
ease: Quad.easeOut,
onComplete: callback,
onCompleteScope: this
}
)
}
fadeOut(callback, delay) {
TweenLite.fromTo(React.findDOMNode(this), 0.5,
{
y: 0,
opacity: 1
},
{
y: '+=100px',
delay: delay,
opacity: 0,
ease: Quad.easeIn,
onComplete: callback,
onCompleteScope: this
}
)
}
componentWillAppear(callback) {
//console.log('RevealText will appear')
this.fadeIn(callback, 1)
}
componentDidAppear() {
//console.log("RevealText did appear")
}
componentWillLeave(callback) {
this.fadeOut(callback, 0)
}
componentDidLeave() {
//console.log('RevealText did leave')
}
componentWillEnter(callback) {
this.fadeIn(callback, 1)
}
componentDidEnter() {
//console.log("RevealText did enter")
}
render() {
return (
<div className='reveal-text'>
{ UI_TEXT[this.props.locale].REVEAL[this.props.tag].TEXT }
</div>
)
}
}
class RevealIcon extends React.Component {
componentWillAppear(callback) {
const HAND_1 = [React.findDOMNode(this.refs.HAND_1),
React.findDOMNode(this.refs.HAND_1_MASK),
React.findDOMNode(this.refs.HAND_1_THUMB)]
const HAND_2 = [React.findDOMNode(this.refs.HAND_2),
React.findDOMNode(this.refs.HAND_2_MASK)]
const HAND_3 = React.findDOMNode(this.refs.HAND_LINES)
const HAND_4 = [React.findDOMNode(this.refs.HAND_LINES_1),
React.findDOMNode(this.refs.HAND_LINES_2),
React.findDOMNode(this.refs.HAND_LINES_3),
React.findDOMNode(this.refs.HAND_LINES_4),
React.findDOMNode(this.refs.HAND_LINES_5),
React.findDOMNode(this.refs.HAND_LINES_6),
React.findDOMNode(this.refs.HAND_LINES_7),
React.findDOMNode(this.refs.HAND_LINES_8)]
let anim = new TimelineMax({
delay: 2,
onComplete: this.props.nextTag,
onCompleteScope: this
})
anim.fromTo(HAND_1, 0.5,
{
y: '-=100px',
x: '+=100px',
opacity: 0
},
{
y: 0,
x: 0,
opacity: 1,
ease: Quad.easeOut
})
.fromTo(HAND_2, 0.5,
{
y: '-=100px',
x: '-=100px',
opacity: 0
},
{
y: 0,
x: 0,
opacity: 1,
ease: Quad.easeOut
}, '-=0.20')
.fromTo(HAND_3, 0.75,
{
scaleX: 0.5,
scaleY: 0.5,
transformOrigin: '50% 50%'
},
{
scaleX: 1,
scaleY: 1,
ease: Quad.easeOut
})
.fromTo(HAND_4, 0.5,
{
opacity: 0,
},
{
opacity: 1,
ease: Quad.easeOut
}, '-=0.75')
.fromTo(HAND_4, 1,
{
'stroke-dasharray': '25px',
'stroke-dashoffset': '0px'
},
{
'stroke-dasharray': '25px',
'stroke-dashoffset': '25px',
ease: Power3.easeOut
}, '-=0.75')
.set({}, {}, '+=1')
// .set is used to lengthen the animation by 1 second
}
componentWillReceiveProps(nextProps) {
console.log('RevealIcon will receive props', 'nextProps.tag: ' + nextProps.tag)
if(nextProps.tag === 1){
// Animation code / reference to method here
} else if (nextProps.tag === 2) {
// Animation code / reference to method here
} else if (nextProps.tag === 3) {
// Animation code / reference to method here
}
}
render() {
return (
<div className='reveal-icon' >
<svg height="200" width="200" viewBox="0, 0, 200, 200">
<path ref="HAND_1" className="fill" d="M146.8,79.9l-55.2,55.2c-1.8,1.8-4.8,1.8-6.7,0c-1.8-1.8-1.8-4.8,0-6.7h0l18.4-18.4l-3.3-3.3
l-18.4,18.4c-0.9,0.9-2.1,1.4-3.3,1.4s-2.5-0.5-3.3-1.4c-0.9-0.9-1.4-2.1-1.4-3.3c0-1.3,0.5-2.5,1.4-3.3L93.3,100L90,96.7
l-18.4,18.4c-1.8,1.8-4.8,1.8-6.7,0c-1.8-1.8-1.8-4.8,0-6.7l41.8-41.8l-3.3-3.3L61.5,105c-3.7,3.7-3.7,9.7,0,13.4
c1.8,1.8,4.3,2.8,6.7,2.8c0.2,0,0.4,0,0.6,0c0,0.2,0,0.4,0,0.6c0,2.5,1,4.9,2.8,6.7c1.8,1.8,4.2,2.8,6.7,2.8c0.2,0,0.4,0,0.6,0
c-0.2,2.6,0.7,5.3,2.7,7.3c1.8,1.8,4.3,2.8,6.7,2.8c2.4,0,4.8-0.9,6.7-2.8l55.2-55.2L146.8,79.9z"/>
<path ref="HAND_2_MASK" className="mask" d="M138.5,105l-22.7-22.7L83.3,49.8L49.8,83.3l32.5,32.5l22.7,22.7
c1.8,1.8,4.3,2.8,6.7,2.8c2.4,0,4.8-0.9,6.7-2.8c2-2,2.9-4.7,2.7-7.3c0.2,0,0.4,0,0.6,0c2.5,0,4.9-1,6.7-2.8
c1.8-1.8,2.8-4.2,2.8-6.7c0-0.2,0-0.4,0-0.6c0.2,0,0.4,0,0.6,0c2.4,0,4.8-0.9,6.7-2.8c1.8-1.8,2.8-4.2,2.8-6.7
C141.2,109.2,140.2,106.8,138.5,105z"/>
<path ref="HAND_2" className="fill" d="M138.5,105L83.3,49.8l-3.3,3.3l55.2,55.2c0.9,0.9,1.4,2.1,1.4,3.3c0,1.3-0.5,2.5-1.4,3.3
c-1.8,1.8-4.8,1.8-6.7,0L110,96.7l-3.3,3.3l18.4,18.4c0.9,0.9,1.4,2.1,1.4,3.3c0,1.3-0.5,2.5-1.4,3.3c-0.9,0.9-2.1,1.4-3.3,1.4
s-2.5-0.5-3.3-1.4L100,106.7l-3.3,3.3l18.4,18.4c1.8,1.8,1.8,4.8,0,6.7c-1.8,1.8-4.8,1.8-6.7,0L53.2,79.9l-3.3,3.3l55.2,55.2
c1.8,1.8,4.3,2.8,6.7,2.8c2.4,0,4.8-0.9,6.7-2.8c2-2,2.9-4.7,2.7-7.3c0.2,0,0.4,0,0.6,0c2.5,0,4.9-1,6.7-2.8
c1.8-1.8,2.8-4.2,2.8-6.7c0-0.2,0-0.4,0-0.6c0.2,0,0.4,0,0.6,0c2.4,0,4.8-0.9,6.7-2.8c1.8-1.8,2.8-4.2,2.8-6.7
C141.2,109.2,140.2,106.8,138.5,105z"/>
<path ref="HAND_1_MASK" className="mask" d="M116.7,49.8l-5,5l-3.3-3.3c-1.8-1.8-4.2-2.8-6.7-2.8c-2.5,0-4.9,1-6.7,2.8
L73.2,73.2c-3.7,3.7-3.7,9.7,0,13.4c1.8,1.8,4.3,2.8,6.7,2.8c2.4,0,4.8-0.9,6.7-2.8l20.1-20.1l5-5l8.4-8.4L116.7,49.8z"/>
<path ref="HAND_1_THUMB" className="fill" d="M116.7,49.8l-5,5l-3.3-3.3l0,0c-1.8-1.8-4.2-2.8-6.7-2.8c-2.5,0-4.9,1-6.7,2.8
L73.2,73.2c-3.7,3.7-3.7,9.7,0,13.4c1.8,1.8,4.3,2.8,6.7,2.8c2.4,0,4.8-0.9,6.7-2.8l20.1-20.1l-3.3-3.3L83.3,83.3
c-1.8,1.8-4.8,1.8-6.7,0s-1.8-4.8,0-6.7l21.7-21.7c0.9-0.9,2.1-1.4,3.3-1.4c1.3,0,2.5,0.5,3.3,1.4l6.7,6.7l8.4-8.4L116.7,49.8z"/>
<g ref="HAND_LINES">
<line ref="HAND_LINES_8" className="stroke" x1="32.6" y1="32.6" x2="49.4" y2="49.4"/>
<line ref="HAND_LINES_7" className="stroke" x1="4.7" y1="100" x2="28.4" y2="100"/>
<line ref="HAND_LINES_6" className="stroke" x1="32.6" y1="167.4" x2="49.4" y2="150.6"/>
<line ref="HAND_LINES_5" className="stroke" x1="100" y1="195.3" x2="100" y2="171.6"/>
<line ref="HAND_LINES_4" className="stroke" x1="167.4" y1="167.4" x2="150.6" y2="150.6"/>
<line ref="HAND_LINES_3" className="stroke" x1="195.3" y1="100" x2="171.6" y2="100"/>
<line ref="HAND_LINES_2" className="stroke" x1="167.4" y1="32.6" x2="150.6" y2="49.4"/>
<line ref="HAND_LINES_1" className="stroke" x1="100" y1="4.7" x2="100" y2="28.4"/>
</g>
</svg>
</div>
)
}
}
class SwitchLocale extends React.Component {
render() {
return (
<button className='switch-locale' onClick={this.props.switchLocale}>
Switch locale
</button>
)
}
}
React.render(<Reveal/>, document.getElementById('react-root'))
</script>
On the left, the animation I currently have created in React, on the right, the icons I still have to implement in the same manner.
Related
How to make particles js to disappear after banner section end in react
I built a banner section with particles js background. and I want start new section but the particles js background is fixed position important. I want to make the particles disappear after the banner end . I opend the developer tools and this is the code of canvas: <canvas data-generated="false" style="width: 100% !important; height: 100% !important; position: fixed !important; z-index: -1 !important; top: 0px !important; left: 0px !important; background-color: rgb(0, 0, 0); pointer-events: none;" aria-hidden="true" width="133" height="640"></canvas> Banner.js code: import { ParticlesBg } from "./ParticlesBg" import './banner.css' export default function Banner() { return ( <section className="banner"> <ParticlesBg className="bg" /> <main> <h1>Hello World</h1> </main> </section> ) } ParticlesBg.js code: import { useCallback } from "react"; import Particles from "react-tsparticles"; import { loadFull } from "tsparticles"; export function ParticlesBg() { const particlesInit = useCallback(async engine => { console.log(engine); // you can initiate the tsParticles instance (engine) here, adding custom shapes or presets // this loads the tsparticles package bundle, it's the easiest method for getting everything ready // starting from v2 you can add only the features you need reducing the bundle size await loadFull(engine); }, []); const particlesLoaded = useCallback(async container => { await console.log(container); }, []); return ( <Particles id="tsparticles" init={particlesInit} loaded={particlesLoaded} options={{ fullScreen: { "enable": true, "zIndex": -1, }, background: { color: { value: "#000000", }, }, fpsLimit: 60, interactivity: { events: { onClick: { enable: false, mode: "push", }, onHover: { enable: false, mode: "repulse", }, resize: true, }, modes: { push: { quantity: 4, }, repulse: { distance: 200, duration: 0.4, }, }, position: "absolute" }, particles: { color: { value: "#ffffff", }, links: { color: "#ffffff", distance: 150, enable: true, opacity: 0.5, width: 1, }, collisions: { enable: true, }, move: { directions: "none", enable: true, outModes: { default: "bounce", }, random: false, speed: 1, straight: false, }, number: { density: { enable: true, area: 800, }, value: 80, }, opacity: { value: 0.5, }, shape: { type: "circle", }, size: { value: { min: 1, max: 5 }, }, }, detectRetina: true, }} /> ); } CSS code: .banner{ width: 100%; height: 100vh; } .banner h1 { font-size: 60px; color: white; } #particles-canvas { position: absolute !important; }
In CSS code: section:not(.banner) { background-color: white; }
Nuxt js Custom cursor event listener not working after route change
I'm currently building a new website for our studio but can't get the custom cursor to work properly. Here's a custom cursor built with gsap and the result was great except when I navigate to another route and back to the home page, the mouseover event stops working and I can't find a reason why. What could be causing this and how could this be fixed? Thank you in advance! Here's the CustomCursor component: <template> <div class="custom-cursor"> <div id="cursor-big" class="custom-cursor__ball custom-cursor__ball--big"></div> <div id="cursor-small" class="custom-cursor__ball custom-cursor__ball--small"></div> </div> </template> <script> import gsap from "gsap"; export default { props: { hoverClass: { type: String, default: 'cursorHover' } }, mounted () { const cursorBig = document.getElementById('cursor-big'), cursorSmall = document.getElementById('cursor-small'), links = document.getElementsByTagName("a"), withClassHover = document.getElementsByClassName(this.hoverClass), withHover = [...links, ...withClassHover]; // Event Listeners document.addEventListener("mousemove", onMouseMove); document.addEventListener("mousedown", onMouseHover); document.addEventListener("mouseup", onMouseHoverOut); document.addEventListener("mouseenter", () => { cursorBig.style.opacity = 1; cursorSmall.style.opacity = 1; }); document.addEventListener("mouseleave", () => { cursorBig.style.opacity = 0; cursorSmall.style.opacity = 0; }); withHover.forEach((element) => { element.addEventListener("mouseover", onMouseHover); element.addEventListener("mouseout", onMouseHoverOut); }) // Event Handlers function onMouseMove(e) { cursorSmall.style.opacity = 1; gsap.to(cursorBig, 0.4, { x: e.clientX - 18.5, y: e.clientY - 18.5 }); gsap.to(cursorSmall, 0.1, { x: e.clientX - 4, y: e.clientY - 4 }); } function onMouseHover() { gsap.to(cursorBig, 0.3, { scale: 3, }); } function onMouseHoverOut() { gsap.to(cursorBig, 0.3, { scale: 1, }); } } }; </script> <style> #media screen and (min-width:1100px) { * { cursor: none !important; } .custom-cursor__ball { position: fixed; top: 0; left: 0; mix-blend-mode: difference; z-index: 99999; opacity: 0; pointer-events: none; transition: opacity 0.5s ease; } .custom-cursor__ball--big { content: ""; width: 35px; height: 35px; background: white; border-radius: 50%; } .custom-cursor__ball--small { content: ""; width: 6px; height: 6px; background: #fff; border-radius: 50%; } } </style>
Moved from comments: Issue: Elements with cursorHover class are not on the DOM after they have been removed when you route off somewhere else. Mounted only fires once. Fix: Handle reinitiating your events onto the dom elements, and destroy your custom event handlers as the route changes. <template> <div class="custom-cursor"> <div id="cursor-big" class="custom-cursor__ball custom-cursor__ball--big" ></div> <div id="cursor-small" class="custom-cursor__ball custom-cursor__ball--small" ></div> </div> </template> <script> import gsap from "gsap"; export default { name: "CustomCursor", props: { hoverClass: { type: String, default: "cursorHover", }, }, data() { return { cursorBig: null, cursorSmall: null, withHover: [], }; }, watch: { "$route.path"() { console.log("route change"); this.destroy(); this.$nextTick(this.init); }, }, mounted() { console.log("mounted"); this.$nextTick(this.init); }, beforeDestroy() { console.log("beforeDestroy"); this.destroy(); }, methods: { init() { console.log("init"); setTimeout(() => { this.cursorBig = document.getElementById("cursor-big"); this.cursorSmall = document.getElementById("cursor-small"); this.withHover = [ ...document.getElementsByTagName("a"), ...document.getElementsByClassName(this.hoverClass), ]; this.withHover.forEach((element) => { element.addEventListener("mouseover", this.onMouseHover); element.addEventListener("mouseout", this.onMouseHoverOut); }); document.addEventListener("mousemove", this.onMouseMove); document.addEventListener("mousedown", this.onMouseHover); document.addEventListener("mouseup", this.onMouseHoverOut); document.addEventListener("mouseenter", this.onMouseEnter); document.addEventListener("mouseleave", this.onMouseLeave); }, 100); }, destroy() { console.log("destroy"); this.withHover.forEach((element) => { element.removeEventListener("mouseover", this.onMouseHover); element.removeEventListener("mouseout", this.onMouseHoverOut); }); document.removeEventListener("mousemove", this.onMouseMove); document.removeEventListener("mousedown", this.onMouseHover); document.removeEventListener("mouseup", this.onMouseHoverOut); document.removeEventListener("mouseenter", this.onMouseEnter); document.removeEventListener("mouseleave", this.onMouseLeave); }, onMouseEnter() { this.cursorBig.style.opacity = 1; this.cursorSmall.style.opacity = 1; }, onMouseLeave() { this.cursorBig.style.opacity = 0; this.cursorSmall.style.opacity = 0; }, onMouseMove(e) { this.cursorSmall.style.opacity = 1; gsap.to(this.cursorBig, 0.4, { x: e.clientX - 18.5, y: e.clientY - 18.5, }); gsap.to(this.cursorSmall, 0.1, { x: e.clientX - 4, y: e.clientY - 4, }); }, onMouseHover() { gsap.to(this.cursorBig, 0.3, { scale: 3, }); }, onMouseHoverOut() { gsap.to(this.cursorBig, 0.3, { scale: 1, }); }, }, }; </script> <style> #media screen and (min-width: 1100px) { * { cursor: none !important; } .custom-cursor__ball { position: fixed; top: 0; left: 0; mix-blend-mode: difference; z-index: 99999; opacity: 0; pointer-events: none; transition: opacity 0.5s ease; } .custom-cursor__ball--big { content: ""; width: 35px; height: 35px; background: black; border-radius: 50%; } .custom-cursor__ball--small { content: ""; width: 6px; height: 6px; background: #000; border-radius: 50%; } } </style>
i currently facing the same issue, but it solved after adding, this.$next.tick() so, i just attached the handler when the components is mounted : mounted() { this.$nextTick(() => { this.transitionController() window.addEventListener('scroll', () => { this.transitionController() }) }) },
Pointer Events API and dragging
I'm trying to implement mobile-only touch UI using Pointer Events API and this just isn't working: https://codepen.io/kyrsquir/full/gOrZEoe The goal is to allow scrolling in the card and dragging the card between expanded and collapsed state within the same UI. I only managed to get dragging by the handle to work in Chrome emulator and iOS Safari. Dragging via card content doesn't work anywhere. On Android Chrome dragging doesn't work at all. The weirdest part is that the same UI works everywhere using touchstart, touchmove and touchend events (https://codepen.io/kyrsquir/full/QWNZzav) but performance of touchmove in Chrome on Android in the presence of scrollbar is so terrible that I'm trying to reimplement it with pointer events. Vue component code: <template> <div class="container"> <div :class="containerClass" :style="containerStyle" #pointerdown="pointerDownHandler" #pointermove="pointerMoveHandler" #pointerup="pointerUpHandler" #pointerover="reportEvent" #pointerenter="reportEvent" #pointercancel="reportEvent" #pointerout="reportEvent" #pointerleave="reportEvent" #gotpointercapture="reportEvent" #lostpointercapture="reportEvent" #transitionend="transitionEndHandler" class="card" > <svg viewBox="0 0 100 20" always-swipeable="true" class="drag-handle"> <polyline points="27,10 73,10" stroke-linecap="round"></polyline> </svg> <div class="scrollbox" ref="scrollbox"> <div :key="item" class="item" v-for="item in items"> {{ item }} </div> </div> </div> </div> </template> <script> export default { data() { return { deltaY: 0, isDraggingByHandle: false, isTransitioning: false, items: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ], offsetHeight: 0, scrollboxHeight: 0, scrollTop: 0, touchAction: null, verticalStates: [ { translateY: 0, }, { translateY: window.innerHeight - 200, }, ], verticalStateIndex: 0, }; }, computed: { activeVerticalState() { return this.verticalStates[this.verticalStateIndex]; }, containerClass() { return { "show-scrollbar": this.isScrollable, transition: this.isTransitioning, }; }, containerStyle() { return { transform: `translateY(${this.translateY}px)`, }; }, isAnySwipeAllowed() { return this.isDraggingByHandle || !this.isScrollable; }, isExpanded() { return this.verticalStateIndex === 0; }, isScrollable() { return this.isExpanded && this.scrollHeight > this.offsetHeight; }, isSwipeDown() { return ( this.deltaY > 0 && (this.isAnySwipeAllowed || this.scrollTop === 0) ); }, isSwipeUp() { return ( this.deltaY < 0 && (this.isAnySwipeAllowed || this.offsetHeight + this.scrollTop === this.scrollHeight) ); }, scrollbox() { return this.$refs.scrollbox; }, translateY() { let translateY = this.activeVerticalState.translateY; if ( this.touchAction === "verticalSwipe" && (this.isSwipeDown || this.isSwipeUp) ) { translateY = translateY + this.deltaY; } return translateY; }, }, mounted() { this.updateScrollboxData(); }, methods: { pointerDownHandler: function ({ clientY, target, pointerId }) { console.log("pointerdown", target, pointerId); target.setPointerCapture(pointerId); this.updateScrollboxData(); this.isDraggingByHandle = Boolean( target.getAttribute("always-swipeable") ); this.touchStartClientY = clientY; this.touchAction = "tap"; }, pointerMoveHandler: function ({ clientY, target, pointerId }) { console.log("pointermove", target, pointerId, this.deltaY); this.deltaY = clientY - this.touchStartClientY; // promote touchAction to swipe or scroll depending on deltas and other variables if (this.touchAction === "tap") { if (this.isSwipeDown || this.isSwipeUp) { this.touchAction = "verticalSwipe"; } else { this.touchAction = "scroll"; this.updateScrollboxData(); } } }, pointerUpHandler: function ({ target, pointerId }) { console.log("pointerup", target, pointerId); target.releasePointerCapture(pointerId); switch (this.touchAction) { case "tap": if (!this.isExpanded) { this.verticalStateIndex = Math.max(this.verticalStateIndex - 1, 0); this.isTransitioning = true; } break; case "verticalSwipe": if (this.isSwipeDown) { this.verticalStateIndex = Math.min( this.verticalStateIndex + 1, this.verticalStates.length - 1 ); } else if (this.isSwipeUp) { this.verticalStateIndex = Math.max(this.verticalStateIndex - 1, 0); } this.isTransitioning = true; this.deltaY = 0; break; } }, reportEvent({ type, target, pointerId }) { console.log(type, target, pointerId); }, transitionEndHandler() { this.touchAction = null; this.isTransitioning = false; this.updateScrollboxData(); }, updateScrollboxData() { const { scrollHeight, offsetHeight, scrollTop } = this.scrollbox; this.offsetHeight = offsetHeight; this.scrollHeight = scrollHeight; this.scrollTop = scrollTop; }, }, }; </script> <style lang="scss" scoped> .container { display: flex; justify-content: center; margin-top: 5vh; overflow: hidden; touch-action: none; .card { pointer-events: all; width: 90vw; height: 100%; touch-action: none; &.transition { transition: transform 0.15s ease-out; } .drag-handle { stroke-width: 5px; stroke: #bfbfc0; width: 100%; height: 30px; background: pink; } .scrollbox { overflow-y: hidden; pointer-events: none; background: green; height: 85vh; .item { margin-bottom: 20px; height: 150px; font-size: 36px; background: yellow; } } &.show-scrollbar .scrollbox { overflow-y: scroll; pointer-events: all; } } } </style>
How to generate Canvas layer with on click button
i have a question - how to draw canvas layer (for example just simple square) with event on click on button in Vue.js? I have stage and on that stage with position x:0, y:0 i want after click on button to generate that square and with drag and drop to position it on that stage? I'm using Konvajs for creating Canvas Can somebody help me? <template> <div id="main"> <h1></h1> <div id="obszarroboczy" style="width: 500px; height: 600px;"> <v-stage ref="stage" :config="configKonva" #dragstart="handleDragstart" #dragend="handleDragend"> <v-layer ref="layer"> <v-star v-for="item in list" :key="item.id" :config="item"></v-star> </v-layer> <v-layer ref="dragLayer"></v-layer> </v-stage> </div> <div class="col-md-6"> <button v-on:click="handleClick" id="more_canvas">More</button> </div> </div> </template> <script> import Vue from "vue"; import axios from "axios"; import draggable from "vuedraggable"; import swal from "sweetalert2"; import VueKonva from "vue-konva"; export default { name: "EnumCurrencyIndex", $mount: "#main", components: { draggable }, data() { return { model: [], editable: true, isDragging: false, delayedDragging: false, type: "currency", editedElement: null, newElement: "", list: [], configKonva: { width: 400, height: 400 }, configCircle: { x: 100, y: 100, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4 }, vm: {} }; }, beforeMount() { this.fetchData(); }, computed: { dragOptions() { return { animation: 0, group: "description", disabled: !this.editable, ghostClass: "ghost" }; }, listString() { return this.model; }, dragCanvas() { return this.model; } }, watch: { $route: "fetchData", isDragging(newValue) { if (newValue) { this.delayedDragging = true; return; } this.$nextTick(() => { this.delayedDragging = false; }); } }, methods: { handleDragstart(starComponent) { var vm = this; const shape = starComponent.getStage(); const dragLayer = vm.$refs.dragLayer.getStage(); const stage = vm.$refs.stage.getStage(); // moving to another layer will improve dragging performance shape.moveTo(dragLayer); stage.draw(); starComponent.config.shadowOffsetX = 15; starComponent.config.shadowOffsetY = 15; starComponent.config.scaleX = starComponent.config.startScale * 1.2; starComponent.config.scaleY = starComponent.config.startScale * 1.2; }, handleDragend(starComponent) { var vm = this; const shape = starComponent.getStage(); const layer = vm.$refs.layer.getStage(); const stage = vm.$refs.stage.getStage(); shape.moveTo(layer); stage.draw(); shape.to({ duration: 0.5, easing: Konva.Easings.ElasticEaseOut, scaleX: starComponent.config.startScale, scaleY: starComponent.config.startScale, shadowOffsetX: 5, shadowOffsetY: 5 }); }, handleClick(configCircle) { var vm = this; const shape = vm.$refs.layer.getStage(); const layer = vm.$refs.layer.getStage(); const stage = vm.$refs.stage.getStage(); console.log(1); layer.add(configCircle); stage.add(layer); }, haveIntersection(r1, r2) { return !( r2.x > r1.x + r1.width || r2.x + r2.width < r1.x || r2.y > r1.y + r1.height || r2.y + r2.height < r1.y ); }, orderList() { this.model = this.model.sort((one, two) => { return one.position - two.position; }); }, onMove({ relatedContext, draggedContext }) { const relatedElement = relatedContext.element; const draggedElement = draggedContext.element; return ( (!relatedElement || !relatedElement.fixed) && !draggedElement.fixed ); }, fetchData() { var vm = this; axios .get(`/api/${this.resource}?type=${this.type}`) .then(function(response) { Vue.set(vm.$data, "model", response.data.model); }) .catch(function(error) { console.log(error); }); } }, mounted() { var box = document.getElementById("obszarroboczy"); this.configKonva.width = box.offsetWidth; this.configKonva.height = box.offsetHeight; var vm = this; for (let n = 0; n < 30; n++) { const scale = Math.random(); const stage = vm.$refs.stage.getStage(); vm.list.push({ x: Math.random() * stage.getWidth(), y: Math.random() * stage.getHeight(), rotation: Math.random() * 180, numPoints: 5, innerRadius: 30, outerRadius: 50, fill: "#89b717", opacity: 0.8, draggable: true, scaleX: scale, scaleY: scale, shadowColor: "black", shadowBlur: 10, shadowOffsetX: 5, shadowOffsetY: 5, shadowOpacity: 0.6, startScale: scale }); }; }, directives: { "element-focus": function(el, binding) { if (binding.value) { el.focus(); } } } }; </script> <style> #obszarroboczy { width: 100px; height: 300px; } .normal { background-color: grey; } .table td { width: 100px; height: 100px; background: white; border: 2px dotted black; max-width: 100px; padding: 5px; } .drag { display: flex; flex-direction: row; } .list { flex-grow: 1; max-width: 47%; margin-right: 40px; } .name { width: 50%; display: inline-block; height: 50px; background: pink; border: 5px green solid; box-sizing: border-box; padding: 5px; } .name.large { width: 100%; } .dragArea { min-height: 100px; } .dragArea img { margin: 3px; cursor: pointer; } </style>
var mainCanvas = new Vue({ el: '#main', // the element where the method wil lrender the canvas to data: { name: 'Vue.js' }, methods: { handleClick: function (event) { // handleClick is the method name for the button var stage = new Konva.Stage({ // this line till the stage.add() line renders the draggable square container: 'obszarroboczy', width: 500, height: 500 }); var layer = new Konva.Layer(); var rect = new Konva.Rect({ x: 0, y: 0, width: 100, height: 100, fill: 'green', stroke: 'black', strokeWidth: 4, draggable: true }); layer.add(rect); stage.add(layer); } } }); I added comments to explain what certain important lines does but you can check out the official KonvaJS Docs in GitHub for a more detailed explanation on what each line above does.
QtQuick2 - custom MessageBox
Does anyone know how to implement custom MessageBox for mobile devices? I've tried to use Window type, but with no luck (it just shows but somewhere out of screen). I appreciate if someone can show me why usage of Window doesn't work. I used also this example. But on mobile devices it doesn't work. Here is my current code, using Window. As said, it doesn't work since it does show out of screen range. import QtQuick 2.4 import QtQuick.Window 2.1 Item{ function showMessage(text, title) { messageBox.text = text; messageBox.title = title; messageBox.visible = true; } Window { id: messageBox modality: Qt.ApplicationModal title: "" visible: false property alias text: messageBoxLabel.text color: parent.color minimumHeight: 100 minimumWidth: 300 Label { anchors.margins: 10 anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.bottom: messageBoxButton.top horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap id: messageBoxLabel text: "" } Button { anchors.margins: 10 id: messageBoxButton anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter text: "Ok" onClicked: messageBox.visible = false } } } Can someone show me why it's working wrong?
MessageBox.qml import QtQuick 2.2 import QtQuick.Controls 1.2 Rectangle { id: mainWrapper color: "#80000000" x: 0; y: 0; width: parent.width; height: parent.height; opacity: 0; Behavior on opacity { NumberAnimation { duration: 500; easing.type: Easing.OutExpo } } visible: opacity > 0 property string text; MouseArea { anchors.fill: parent; preventStealing: true } signal finished(bool ok); function init() { opacity = 1; msgB.scale = 1.0; } Rectangle { id: msgB color: "#323232" gradient: Gradient { GradientStop { position: 0; color: "#323232" } GradientStop { position: 1; color: "#252525" } } //radius: 7 width: parent.width * 0.4; height: cont.height + 20 * 2; anchors.centerIn: parent; scale: 0.6 Behavior on scale { NumberAnimation { duration: 500; easing.type: Easing.OutExpo } } Behavior on height { NumberAnimation { duration: 500; easing.type: Easing.OutExpo } } Column { id: cont width: parent.width; y: 20; spacing: 20; Text { color: "#ffffff" horizontalAlignment: Text.AlignHCenter anchors.horizontalCenter: parent.horizontalCenter font { bold: false; pixelSize: 21; } wrapMode: Text.WordWrap; text: mainWrapper.text; } Button { anchors.margins: 10 anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter text: "OK" onClicked: { mainWrapper.opacity = 0; msgB.scale = 0.6; mainWrapper.finished(true); } } } } } Somewhere in main.qml file (window is the id of main.qml element): function message(msg, finished) { var alert = Qt.createComponent("MessageBox.qml").createObject(window, { text: msg }); alert.onFinished.connect(function(ok) { if (ok) { if (finished) finished(); } alert.destroy(500); }); alert.init(); return alert; } Use it like this: Button { ... onClicked: { message("Hello world", function() { console.log("OK clicked"); }); } }
Thanks all for answers and comments, summarizing above I created element without Window type, but with contentItem property. It's very raw element, but usable like Dialog as suggested by BaCaRoZzo or Window as in Mechan example. Here is source: main.qml import QtQuick 2.2 import QtQuick.Controls 1.1 import QtQuick.Window 2.2 ApplicationWindow { title: qsTr("Hello World") width: Screen.width height: Screen.height visible: true id: win color: brPalette.charcoal BreezeQuickMessageBox{ id: mbox palette: brPalette contentItem: Rectangle{ color: "lightblue" anchors.fill: parent BreezeQuickButton{ id: btn anchors { verticalCenter: parent.verticalCenter horizontalCenter: parent.horizontalCenter } palette: brPalette gradiented: false onClicked: { mbox.hide() } } } } /* Another bunch of code */ BreezeQuickPalette{ id: brPalette theme: "dark" } } BreezeQuickMessageBox.qml import QtQuick 2.4 Item { id: root property BreezeQuickPalette palette: BreezeQuickPalette property bool __buttonGradiented: false property string title: "Message Box" property Item contentItem anchors.fill: parent Behavior on opacity { NumberAnimation{ duration: 250 } } opacity: 0 visible: opacity > 0 z: parent.z + 100 BreezeQuickPalette{ id: __palette theme: palette.theme } Rectangle { id: window width: parent.width height: parent.height*0.4 anchors { verticalCenter: parent.verticalCenter } z: parent.z + 1 color: palette.charcoal Item { id: content width: parent.width anchors { top: titleText.bottom bottom: line.top horizontalCenter: parent.horizontalCenter topMargin: 8 bottomMargin: 8 } children: contentItem } Rectangle{ id: line width: parent.width anchors{ bottom: buttonArea.top horizontalCenter: parent.horizontalCenter } height: 1 color: palette.focusColor } Text{ id: titleText font.pointSize: buttonOk.font.pointSize color: palette.normalText text: title anchors { top: parent.top horizontalCenter: parent.horizontalCenter topMargin: 16 } verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } Rectangle{ id: buttonArea width: parent.width anchors{ horizontalCenter: parent.horizontalCenter bottom: window.bottom } height: buttonOk.height*1.2 color: "transparent" } BreezeQuickButton { id: buttonOk caption: "Ok" width: 128 palette: __palette gradiented: __buttonGradiented anchors{ horizontalCenter: parent.horizontalCenter verticalCenter: buttonArea.verticalCenter } onClicked: { root.hide() } } } Rectangle{ id: shadow anchors.fill: parent z: parent.z color: palette.shadeBlack opacity: 0.4 MouseArea{ id: rootArea anchors.fill: parent hoverEnabled: true } gradient: Gradient { GradientStop { position: 0.0; color: palette.black } GradientStop { position: 0.1; color: palette.shadeBlack } GradientStop { position: 0.3; color: palette.grey } GradientStop { position: 0.7; color: palette.grey } GradientStop { position: 0.9; color: palette.shadeBlack } GradientStop { position: 1.0; color: palette.black } } } function show (title, message) { root.opacity = 1 } function hide () { root.opacity = 0 } } And actual look for Android: