(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[16234],{21934:(e,t,n)=>{"use strict";n.d(t,{ZP:()=>o.default});var o=n(98074)},56073:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});let o=(0,n(64833).z)("code")({name:"InlineCode",class:"i165vvr1",propsAsIs:!1});n(67787)},72099:(e,t,n)=>{"use strict";n.d(t,{H:()=>o.ListItem,Z:()=>o.default});var o=n(856)},69560:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o.default});var o=n(83928)},16234:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>$});var o=n(67841),i=n(79894),s=n(40996),r=n(21934),a=n(72099),l=n(5928),h=n(56073),c=n(56441),d=n(79477),u=n(64833),p=n(74631);let x=(0,u.z)("svg")({name:"Svg",class:"slo7d8",propsAsIs:!1}),g=function(e){let{width:t,height:n,squiggleWidth:i=5,phase:s=1,...r}=e,a=Math.round(t/i),l=t/a,h=n/2,c=(0,p.w6)(0,a-1).reduce((e,t)=>{let n=t*l,o=h+l/2*(t%2==0?1:-1),i="Q ".concat(n+l/2,",").concat(o," ").concat(n+l,",").concat(h);return"".concat(e," ").concat(i)},"M ".concat(0,",").concat(h));return(0,o.jsx)(x,{width:a*l,height:n,children:(0,o.jsx)("path",{d:c,fill:"none",strokeLinecap:"round",...r})})};n(35287);let w=function(e){let{width:t=100,height:n=50,style:i={},...s}=e;return(0,o.jsx)(v,{...s,style:{...i,maxWidth:t},children:(0,o.jsx)(g,{width:t,height:n,squiggleWidth:10,stroke:"var(--color-secondary)",strokeWidth:2})})},v=(0,u.z)("div")({name:"Wrapper",class:"wt4tu36",propsAsIs:!1});n(47108);var m=n(49722),f=n(75865),y=n(32324),b=n(69560),j=n(62867),Z=n(55158);let k=()=>window.innerWidth<500?"mobile":"desktop";class I extends d.PureComponent{render(){let{points:e,viewBoxWidth:t,viewBoxHeight:n,strokeColor:i,strokeWidth:s,grabbable:r}=this.props,[a,l,h,c]=e,d=void 0!==c?"cubic":"quadratic",u="cubic"===d?"\n M ".concat(a[0],",").concat(a[1],"\n C ").concat(l[0],",").concat(l[1]," ").concat(h[0],",").concat(h[1]," ").concat(c[0],",").concat(c[1],"\n "):"\n M ".concat(a[0],",").concat(a[1],"\n Q ").concat(l[0],",").concat(l[1]," ").concat(h[0],",").concat(h[1],"\n "),p="cubic"===d?c:h,x="cubic"===d?"p4":"p3",g="mobile"===k();return(0,o.jsxs)(B,{viewBox:"0 0 ".concat(t," ").concat(n),ref:e=>this.node=e,onMouseMove:this.handleDrag,onTouchMove:this.handleDrag,onMouseUp:this.handleRelease,onTouchEnd:this.handleRelease,children:[(0,o.jsx)(W,{x1:a[0],y1:a[1],x2:l[0],y2:l[1]}),"quadratic"===d&&(0,o.jsx)(W,{x1:l[0],y1:l[1],x2:h[0],y2:h[1]}),"cubic"===d&&(0,o.jsx)(W,{x1:h[0],y1:h[1],x2:c[0],y2:c[1]}),(0,o.jsx)("path",{d:u,fill:"none",stroke:i,strokeWidth:s}),(0,o.jsx)(T,{cx:a[0],cy:a[1],onMouseDown:this.handleSelectPoint("p1"),onTouchStart:this.handleSelectPoint("p1"),"data-grabbable":String(r),rx:g?40:15,ry:g?40:15}),(0,o.jsx)(S,{cx:l[0],cy:l[1],onMouseDown:this.handleSelectPoint("p2"),onTouchStart:this.handleSelectPoint("p2"),"data-grabbable":String(r),isMobile:g}),"cubic"===d&&(0,o.jsx)(S,{cx:h[0],cy:h[1],onMouseDown:this.handleSelectPoint("p3"),onTouchStart:this.handleSelectPoint("p3"),"data-grabbable":String(r),isMobile:g}),(0,o.jsx)(T,{cx:p[0],cy:p[1],onMouseDown:this.handleSelectPoint(x),onTouchStart:this.handleSelectPoint(x),"data-grabbable":String(r),rx:g?40:15,ry:g?40:15})]})}constructor(...e){super(...e),this.state={draggingPointId:null},this.handleSelectPoint=e=>()=>{this.props.grabbable&&this.setState({draggingPointId:e})},this.handleRelease=()=>{this.setState({draggingPointId:null})},this.handleDrag=e=>{let t,n;if(e.touches){e.preventDefault();let o=e.touches[0];[t,n]=[o.clientX,o.clientY]}else[t,n]=[e.clientX,e.clientY];let{viewBoxWidth:o,viewBoxHeight:i,updatePoint:s,grabbable:r}=this.props,{draggingPointId:a}=this.state;if(!a||!r||!s)return;let l=this.node.getBoundingClientRect(),h=[t-l.left,n-l.top];s(a,[h[0]*o/l.width,h[1]*i/l.height])}}}I.defaultProps={viewBoxWidth:1e3,viewBoxHeight:250,strokeColor:"var(--color-secondary)",strokeWidth:6,grabbable:!0};let S=e=>{let{cx:t,cy:n,onMouseDown:i,onTouchStart:s,isMobile:r,...a}=e;return(0,o.jsxs)("g",{children:[(0,o.jsx)(z,{cx:t,cy:n,rx:r?20:8,ry:r?20:8,...a}),(0,o.jsx)(_,{cx:t,cy:n,rx:r?40:25,ry:r?40:25,onMouseDown:i,onTouchStart:s,...a})]})},B=(0,u.z)("svg")({name:"Svg",class:"ss8jhfh",propsAsIs:!1}),P=(0,u.z)("ellipse")({name:"Point",class:"p1ytwiv5",propsAsIs:!0}),T=(0,u.z)(P)({name:"EndPoint",class:"e17jydvx",propsAsIs:!0}),z=(0,u.z)(P)({name:"VisibleControlPoint",class:"vhow9b3",propsAsIs:!0}),_=(0,u.z)(P)({name:"InvisibleHandle",class:"izp3bdj",propsAsIs:!0}),W=(0,u.z)("line")({name:"ControlLine",class:"cvm5f8k",propsAsIs:!0});n(16254);class M extends d.PureComponent{render(){let{allowToggle:e}=this.props,{viewBoxWidth:t,viewBoxHeight:n,p1:i,p2:s,p3:r,p4:a,type:l}=this.state;return e?(0,o.jsx)(Z.ZP,{initialValues:{type:this.props.initialType},controls:[(0,o.jsx)(Z.Xr,{id:"type",label:"Type",options:[{value:"quadratic",label:"Quadratic"},{value:"cubic",label:"Cubic"}]},"type")],children:e=>{let{type:l}=e;return(0,o.jsx)(I,{viewBoxWidth:t,viewBoxHeight:n,points:"quadratic"===l?[i,s,a]:[i,s,r,a],updatePoint:this.handleUpdatePoint})}}):(0,o.jsx)(C,{children:(0,o.jsx)(I,{viewBoxWidth:t,viewBoxHeight:n,points:"quadratic"===l?[i,s,a]:[i,s,r,a],updatePoint:this.handleUpdatePoint})})}constructor(...e){super(...e),this.state={viewBoxWidth:1e3,viewBoxHeight:400,p1:this.props.p1||[25,200],p2:this.props.p2||[500,200],p3:this.props.p3||[800,200],p4:this.props.p4||[975,200],type:this.props.initialType,allowToggle:!1},this.handleUpdatePoint=(e,t)=>{"quadratic"===this.state.type&&"p3"===e&&(e="p4"),this.setState({[e]:t})}}}M.defaultProps={initialType:"quadratic",allowToggle:!1};let C=(0,u.z)(j.Z)({name:"Wrapper",class:"wwtarvq",propsAsIs:!0});(0,u.z)("div")({name:"DemoWrapper",class:"dkhix0k",propsAsIs:!1}),(0,u.z)("div")({name:"Controls",class:"c1j6dy86",propsAsIs:!1}),(0,u.z)("div")({name:"RadioButtonsWrapper",class:"r14tgjk5",propsAsIs:!1}),(0,u.z)("label")({name:"Label",class:"l1xfqlvi",propsAsIs:!1}),n(35646);class L extends d.PureComponent{componentDidMount(){this.intervalId=window.setInterval(this.tick,2e3)}componentWillUnmount(){window.clearInterval(this.intervalId)}render(){let{flattened:e}=this.state;return(0,o.jsx)(R,{children:(0,o.jsx)(I,{viewBoxWidth:1e3,viewBoxHeight:400,points:[[25,200],[333,e?200:0],[666,e?200:400],[975,200]],grabbable:!1})})}constructor(...e){super(...e),this.state={flattened:!0},this.tick=()=>{window.matchMedia("(prefers-reduced-motion: no-preference)").matches&&this.setState({flattened:!this.state.flattened})}}}let R=(0,u.z)(j.Z)({name:"Wrapper",class:"wdv8tbs",propsAsIs:!0});n(3379);let A=(0,u.z)("svg")({name:"Svg",class:"s1glbvt1",propsAsIs:!1}),H=()=>(0,o.jsxs)(A,{viewBox:"0 0 440 440",children:[(0,p.w6)(1,22).map(e=>(0,o.jsx)("line",{x1:20*e,y1:0,x2:20*e,y2:440,stroke:"var(--color-gray-200)",strokeWidth:1},e)),(0,p.w6)(1,22).map(e=>(0,o.jsx)("line",{x1:0,y1:20*e,x2:440,y2:20*e,stroke:"var(--color-gray-200)",strokeWidth:1},e)),(0,o.jsx)("line",{x1:220,y1:120,x2:320,y2:120,stroke:"var(--color-gray-300)",strokeWidth:2,strokeDasharray:5,strokeLinecap:"round"}),(0,o.jsx)("line",{x1:320,y1:120,x2:320,y2:220,stroke:"var(--color-gray-300)",strokeWidth:2,strokeDasharray:5,strokeLinecap:"round"}),(0,o.jsx)("line",{x1:0,y1:220,x2:440,y2:220,stroke:"var(--color-gray-800)",strokeWidth:3,strokeLinecap:"round"}),(0,o.jsx)("line",{x1:220,y1:0,x2:220,y2:440,stroke:"var(--color-gray-800)",strokeWidth:3,strokeLinecap:"round"}),(0,o.jsx)("line",{x1:220,y1:20,x2:210,y2:20,stroke:"var(--color-gray-800)",strokeWidth:3,strokeLinecap:"round"}),(0,o.jsx)("text",{fill:"var(--color-text)",textAnchor:"end",x:200,y:20,dy:5,children:"200"}),(0,o.jsx)("line",{x1:220,y1:120,x2:210,y2:120,stroke:"var(--color-gray-800)",strokeWidth:3,strokeLinecap:"round"}),(0,o.jsx)("text",{fill:"var(--color-text)",textAnchor:"end",x:200,y:120,dy:5,children:"100"}),(0,o.jsx)("line",{x1:320,y1:220,x2:320,y2:230,stroke:"var(--color-gray-800)",strokeWidth:3,strokeLinecap:"round"}),(0,o.jsx)("text",{fill:"var(--color-text)",textAnchor:"middle",x:320,y:240,dy:8,children:"0.5"}),(0,o.jsx)("line",{x1:420,y1:220,x2:420,y2:230,stroke:"var(--color-gray-800)",strokeWidth:3,strokeLinecap:"round"}),(0,o.jsx)("text",{fill:"var(--color-text)",textAnchor:"middle",x:420,y:240,dy:8,children:"1.0"}),(0,o.jsx)("line",{x1:220,y1:20,x2:420,y2:220,stroke:"var(--color-primary)",strokeWidth:4,strokeLinecap:"round"})]});n(70489);class D extends d.Component{render(){let{height:e,color:t}=this.props;return(0,o.jsx)(V,{width:"100%",height:e,ref:e=>this.node=e,viewBox:"0 0 720 200",children:(0,o.jsx)("path",{d:this.calculatePathForCurve(),stroke:t,strokeWidth:12,strokeLinecap:"round",fill:"none"})})}constructor(...e){super(...e),this.calculatePathForCurve=e=>{let{percentStraightened:t}=this.props,n=t/100,o=Math.round((0,p.Gy)(0,0,n)),i=Math.round((0,p.Gy)(350,0,n)),s=Math.round((0,p.Gy)(-90,0,n)),r=Math.round((0,p.Gy)(200,0,n));return"\n M 0,".concat(o,"\n C 240,").concat(i," 432,").concat(s," 720,").concat(r,"\n ")}}}D.defaultProps={height:200,buffer:0};let V=(0,u.z)("svg")({name:"Svg",class:"sv24af8",propsAsIs:!1});n(49080);let G=function(){return(0,o.jsx)(Z.ZP,{id:"initial-curve",initialValues:{percentStraightened:0},controls:[(0,o.jsx)(Z.vn,{id:"percentStraightened",label:"Percent Straightened",min:0,max:100},"percentStraightened")],children:e=>{let{percentStraightened:t}=e;return(0,o.jsx)(D,{percentStraightened:t,color:"var(--color-primary)"})}})},q=(0,i.default)(()=>Promise.all([n.e(46288),n.e(92203),n.e(28061)]).then(n.bind(n,28061)),{loadableGenerated:{webpack:()=>[28061]},ssr:!1}),$=function(){return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(s.Z,{children:"In 2018, I had a problem. I had been publishing my blog posts on Medium, but I was getting frustrated by the lack of control. I had all these whimsical ideas bouncing around in my head, and no outlet! And so, I decided to build my own blog."}),(0,o.jsx)(s.Z,{children:"Looking back, the first version of my blog was pretty cringy, but there was one cool feature that I'm still pretty happy with. The lime-green hero had these dramatic swoops, and they flattened out on scroll:"}),(0,o.jsx)(b.Z,{src:"/videos/dynamic-bezier-curves/original-blog-hero.mp4",width:681,height:355}),(0,o.jsx)(s.Z,{children:"In this blog post, I'm going to show you how I did this. We’ll learn how to create swoopy SVGs that can be updated dynamically:"}),(0,o.jsx)(G,{}),(0,o.jsxs)(r.ZP,{type:"warning",title:"Blast from the past",children:[(0,o.jsx)(s.Z,{children:"This post was originally published in 2018, and honestly, it's far from my best work \uD83D\uDE05. In 2024, I migrated it to my new blog, fixing some of the technical issues, but without revising the content."}),(0,o.jsx)(s.Z,{children:"In terms of whether the code is outdated: the SVG stuff is still relevant. The React code, however, uses the older “class component” syntax, and should be updated before use."})]}),(0,o.jsx)(l.SectionHeading,{anchorId:"understanding-svg-paths",children:"A Quick SVG Refresher"}),(0,o.jsx)(s.Z,{children:"For achieving this effect, we'll use SVG. We could also use HTML Canvas, but I generally prefer to work with SVG. It's more React-like in its API, there's less complexity in setting it up, and it's more a11y-friendly."}),(0,o.jsxs)(s.Z,{children:["While doing a deep dive into SVG is beyond the scope of this post (I'd recommend the"," ",(0,o.jsx)(c.Z,{href:"https://www.w3schools.com/graphics/svg_intro.asp",children:"W3Schools tutorial"})," ","for that), we'll cover the basics, and show how to create some shapes from scratch. Experienced SVG-ers can jump to"," ",(0,o.jsx)(c.Z,{href:"#intro-to-bezier-curves",children:"the next section"}),"."]}),(0,o.jsxs)(s.Z,{children:["The simplest form of SVG drawings use shape elements, like"," ",(0,o.jsx)(h.Z,{children:""})," or"," ",(0,o.jsx)(h.Z,{children:""}),"."]}),(0,o.jsx)(s.Z,{children:"Try tweaking some of the values below, to build an understanding of how SVG shapes work:"}),(0,o.jsx)(y.default,{id:"basic-shapes",html:X,layoutMode:"tabbed",centered:!0}),(0,o.jsx)(s.Z,{children:"These shapes are straightforward and declarative, but that simplicity comes at the cost of flexibility; you can only create a handful of different shapes."}),(0,o.jsxs)(s.Z,{children:["To do neat curvy things, we need to use the"," ",(0,o.jsx)(h.Z,{children:""})," element. This swiss-army-knife of an SVG primitive lets you specify a sequence of steps to execute, in a seemingly-inscrutable bundle of letters and numbers:"]}),(0,o.jsx)(y.default,{id:"intro-path",html:Y,layoutMode:"tabbed",centered:!0}),(0,o.jsx)(s.Z,{children:"The interactive code snippet above uses 2 commands:"}),(0,o.jsxs)(a.Z,{children:[(0,o.jsxs)(a.Z.ListItem,{children:[(0,o.jsx)(h.Z,{children:"M"}),", which instructs the path to"," ",(0,o.jsx)(m.Z,{children:"move"})," to a specific coordinate."]}),(0,o.jsxs)(a.Z.ListItem,{children:[(0,o.jsx)(h.Z,{children:"L"}),", which instructs the path to create a ",(0,o.jsx)(m.Z,{children:"line"})," from the current position to the specified coordinate."]})]}),(0,o.jsxs)(s.Z,{children:["After the commands ",(0,o.jsx)(h.Z,{children:"M"})," and"," ",(0,o.jsx)(h.Z,{children:"L"}),', we see some numbers. These can be thought of as "arguments" for the commands. In this case, the arguments are coordinates; both commands require a single X/Y pair.']}),(0,o.jsxs)(s.Z,{children:['In other words, we can read the above path as: "Move to'," ",(0,o.jsx)(h.Z,{children:"{x: 100, y: 100}"}),", then draw a line to ",(0,o.jsx)(h.Z,{children:"{x: 200, y: 100}"}),'", and so on.']}),(0,o.jsxs)(s.Z,{children:["The coordinate system is relative to the values specified in the ",(0,o.jsx)(h.Z,{children:"viewBox"}),". The current viewbox specifies that the viewable area has a top-left corner of 0/0, a width of 300, and a height of 300. So all of the coordinates specified in the ",(0,o.jsx)(h.Z,{children:"path"})," are within that 300x300 box."]}),(0,o.jsxs)(s.Z,{children:["The ",(0,o.jsx)(h.Z,{children:"viewBox"})," is what makes SVGs scalable; we can make our SVG any size we like, and everything will scale naturally, since the elements within our SVG are relative to this 300x300 box."]}),(0,o.jsxs)(s.Z,{children:["The ",(0,o.jsx)(h.Z,{children:"path"})," element features"," ",(0,o.jsx)(c.Z,{href:"https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths",children:"quite a number"})," ","of these commands. There are two that are relevant for our purposes:"]}),(0,o.jsxs)(a.Z,{children:[(0,o.jsxs)(a.Z.ListItem,{children:[(0,o.jsx)(h.Z,{children:"Q"}),", which instructs the path to create a ",(0,o.jsx)(m.Z,{children:"quadratic"})," B\xe9zier curve."]}),(0,o.jsxs)(a.Z.ListItem,{children:[(0,o.jsx)(h.Z,{children:"C"}),", which instructs the path to create a ",(0,o.jsx)(m.Z,{children:"cubic"})," B\xe9zier curve."]})]}),(0,o.jsx)(f.Z,{size:80}),(0,o.jsx)(l.SectionHeading,{anchorId:"intro-to-bezier-curves",children:"Intro to B\xe9zier Curves"}),(0,o.jsx)(s.Z,{children:"B\xe9zier curves are surprisingly common. Due to their versatility, they're a staple in most graphics software like Photoshop, but they're also used as timing functions: if you've ever used non-linear CSS transitions (like the default \"ease\"), you've already worked with B\xe9zier curves!"}),(0,o.jsx)(s.Z,{children:"But what are they, and how do they work?"}),(0,o.jsxs)(s.Z,{children:["A B\xe9zier curve is essentially a line from a"," ",(0,o.jsx)(m.Z,{children:"start point"})," to an ",(0,o.jsx)(m.Z,{children:"end point"})," that is acted upon by one or more ",(0,o.jsx)(m.Z,{children:"control points"}),". A control point curves the line towards it, as if the control point was pulling it in its direction."]}),(0,o.jsx)(s.Z,{children:"The following line looks like a straight line, but check out what happens when you move the points around—try dragging the middle control point up and down."}),(0,o.jsx)(M,{id:"initial",initialType:"quadratic"}),(0,o.jsxs)(s.Z,{children:["The line above is a ",(0,o.jsx)(m.Z,{children:"quadratic"})," B\xe9zier curve; this means that it has a ",(0,o.jsx)("strong",{children:"single control point"}),". I'm guessing it gets its name from the fact that you can create parabola-like shapes with it:"]}),(0,o.jsx)(M,{id:"parabola",initialType:"quadratic",p1:[400,15],p2:[500,395],p4:[600,15]}),(0,o.jsxs)(s.Z,{children:["A ",(0,o.jsx)(m.Z,{children:"cubic"})," B\xe9zier curve, in contrast, has"," ",(0,o.jsx)("strong",{children:"two"})," control points. This allows for much more interesting curves:"]}),(0,o.jsx)(M,{id:"toggleable",allowToggle:!0,initialType:"cubic",p1:[25,25],p2:[333,375],p3:[666,25],p4:[975,375]}),(0,o.jsxs)(s.Z,{children:["The syntax for B\xe9zier curves in SVG"," ",(0,o.jsx)(h.Z,{children:"path"})," definitions is a little counter-intuitive, but it looks like this. The following code is written in JSX, rather than HTML, so that I can name the variables:"]}),(0,o.jsx)(q,{includeContentStyles:!0,code:U,lang:"jsx"}),(0,o.jsxs)(s.Z,{children:["The thing that makes this counter-intuitive, to me at least, is that the ",(0,o.jsx)(h.Z,{children:"startPoint"})," is inferred in the ",(0,o.jsx)(h.Z,{children:"Q"})," command; while there are 3 points needed for a quadratic B\xe9zier curve, only 2 points are passed as arguments to ",(0,o.jsx)(h.Z,{children:"Q"}),"."]}),(0,o.jsxs)(s.Z,{children:["Similarly, for a cubic B\xe9zier curve, only the control points and the end point are provided to the"," ",(0,o.jsx)(h.Z,{children:"C"})," command."]}),(0,o.jsx)(s.Z,{children:"This syntax does mean that curves can conveniently be chained together, as one curve starts where the last one ends:"}),(0,o.jsx)(y.default,{id:"chained-curves",html:O,layoutMode:"tabbed",centered:!0}),(0,o.jsx)(s.Z,{children:"Ok, I think that's enough playing with vanilla SVGs. Let's see how we can leverage React to make these curves dynamic!"}),(0,o.jsx)(l.SectionHeading,{anchorId:"bezier-curves-in-react",children:"B\xe9zier Curves in React"}),(0,o.jsx)(s.Z,{children:"Up to this point, we've been looking at static SVGs. How do we make them change, over time or based on user input?"}),(0,o.jsx)(s.Z,{children:'Well, in keeping with the "meta" theme of this blog post, why not examine the draggable-with-lines B\xe9zier curves from earlier in this post?'}),(0,o.jsx)(s.Z,{children:"There's a fair bit of code to manage this, even in this slightly-simplified snippet. I've annotated it heavily, which hopefully makes things easier to parse. \uD83E\uDD1E"}),(0,o.jsx)(q,{includeContentStyles:!0,lang:"jsx",code:E}),(0,o.jsx)(s.Z,{children:"To summarize how this works:"}),(0,o.jsxs)(a.Z,{children:[(0,o.jsxs)(a.Z.ListItem,{children:["React holds variables in component state for"," ",(0,o.jsx)(h.Z,{children:"startPoint"}),","," ",(0,o.jsx)(h.Z,{children:"controlPoint"}),", and"," ",(0,o.jsx)(h.Z,{children:"endPoint"}),"."]}),(0,o.jsxs)(a.Z.ListItem,{children:["In the ",(0,o.jsx)(m.Z,{children:"render"})," method, we build the instructions for the ",(0,o.jsx)(h.Z,{children:"path"})," using these state variables."]}),(0,o.jsxs)(a.Z.ListItem,{children:["When the user clicks or taps on one of the points, we update the state to keep track of which point is moving with"," ",(0,o.jsx)(h.Z,{children:"draggingPointId"}),"."]}),(0,o.jsx)(a.Z.ListItem,{children:"As the user moves the mouse (or finger) across the SVG's surface, we do some calculations to figure out where the currently-dragging point needs to move to. This is made complex by the fact that SVGs have their own internal coordinate system (viewBox), and so we have to translate the on-screen pixels to this system."}),(0,o.jsxs)(a.Z.ListItem,{children:["Once we have the new X/Y coordinate for the active point,"," ",(0,o.jsx)(h.Z,{children:"setState"})," lets React know about this state change, and the component re-renders, which causes the"," ",(0,o.jsx)(h.Z,{children:"path"})," to be re-calculated."]})]}),(0,o.jsx)(f.Z,{size:25}),(0,o.jsx)(l.SectionHeading,{anchorId:"a-note-on-performance",children:"A note on performance"}),(0,o.jsxs)(s.Z,{children:["By using React's update cycle to manage the point coordinates, there is added overhead of letting React run its reconciliation cycle on every"," ",(0,o.jsx)(h.Z,{children:"mousemove"}),". Is this prohibitively expensive?"]}),(0,o.jsx)(s.Z,{children:"The answer is that it depends. React's reconciliation can be surprisingly fast, especially when dealing with such a small tree (after all, the only thing that needs to be diffed is an SVG). Especially in \"production\" mode, when React doesn't have to do a lot of dev warning checks, this process can take fractions of a millisecond."}),(0,o.jsxs)(s.Z,{children:["I wrote an"," ",(0,o.jsx)(c.Z,{href:"https://github.com/joshwcomeau/blog/blob/master/src/pages/posts/dynamic-bezier-curves/code/optimized-react-bezier.example",children:"alternative implementation"})," ",'that updates the DOM directly. It does run faster (about 50% faster in my quick test), but both implementations still clock in under 1ms on modern high-end hardware. On the cheapest Chromebook I could find, the "unoptimized" one still averaged 50fps or so.']}),(0,o.jsx)(f.Z,{size:80}),(0,o.jsx)(l.SectionHeading,{anchorId:"curve-interpolation",children:"Curve Interpolation"}),(0,o.jsx)(s.Z,{children:"I seem to have gotten a little side-tracked! Our original goal was to create a B\xe9zier curve that flattens itself on scroll."}),(0,o.jsx)(s.Z,{children:"Given what we've gone over so far, we have almost all of the tools we need to solve this problem! A B\xe9zier curve with its control point(s) directly between the start and end points is actually a straight line! So we need to transition the control points from their curvy values to a flat value."}),(0,o.jsx)(L,{}),(0,o.jsx)(f.Z,{size:32}),(0,o.jsxs)(s.Z,{children:["We need a way to ",(0,o.jsx)(m.Z,{children:"interpolate values"}),". We know where the control points should be at 0% and 100%, but what about when the user is 25% scrolled through the content?"]}),(0,o.jsx)(s.Z,{children:"While we could be fancy and ease the transition, a linear transformation works just fine for our purposes. So when the user is 50% scrolled through the content, the control points will be 50% of the way between their initial curvy value, and the flat-line value."}),(0,o.jsxs)(s.Z,{children:["For this, some secondary-school maths will come in handy. If you're already up to speed on interpolation, you can"," ",(0,o.jsx)(c.Z,{href:"#handling-scroll-in-react",children:"skip this bit"}),"."]}),(0,o.jsxs)(s.Z,{children:["If you plumb the depths of your memory, you may remember how to calculate the ",(0,o.jsx)(m.Z,{children:"slope"})," of a line. The slope tells you how the line changes over time. We calculate it by dividing the ",(0,o.jsx)("strong",{children:"change in y"})," over the"," ",(0,o.jsx)("strong",{children:"change in x"}),":"]}),(0,o.jsxs)(s.Z,{style:{textAlign:"center"},children:[(0,o.jsx)(h.Z,{children:"slope"})," ="," ",(0,o.jsx)(h.Z,{children:"(y2 - y1) / (x2 - x1)"})," ="," ",(0,o.jsx)(h.Z,{children:"(Δy) / (Δx)"})]}),(0,o.jsx)(s.Z,{children:"There's also this rascal, the linear equation formula. This allows us to graph a straight line, and figure out the y value for a given x value. By convention, slope is given the variable a:"}),(0,o.jsx)(s.Z,{style:{textAlign:"center"},children:(0,o.jsx)(h.Z,{children:"y = ax + b"})}),(0,o.jsx)(s.Z,{children:"How does this relate to interpolation? Well, let's imagine that our B\xe9zier curve's control point, when it's all curvy, is 200 pixels away from its flattened position, so we'll give it an initial y value of 200. The x in this case is really a measure of progress, so we'll have it range from 0 (completely curvy) to 1 (completely flat). If we graph this line, we get this:"}),(0,o.jsx)(H,{}),(0,o.jsx)(s.Z,{children:'To clarify, this line represents the range of possible y values for a quadratic B\xe9zier curve\'s control point. Our x values represent the degree of "flattening"; this is useful to us because we want to be able to provide an x value like 0.46, and figure out what the corresponding y value is (our x value will come from user input, like the percentage scrolled through the viewport).'}),(0,o.jsxs)(s.Z,{children:["To make our formula work, we need to know at least 2 points on this line. Thankfully, we do! We know that the initial position, fully curved, is at"," ",(0,o.jsx)(h.Z,{children:"{ x: 0, y: 200 }"}),", and we know that the curve becomes fully flattened at"," ",(0,o.jsx)(h.Z,{children:"{ x: 1, y: 0 }"}),"."]}),(0,o.jsxs)(a.Z,{children:[(0,o.jsxs)(a.Z.ListItem,{children:["The slope would be equal to"," ",(0,o.jsx)(h.Z,{children:"(Δy) / (Δx)"})," ="," ",(0,o.jsx)(h.Z,{children:"(0 - 200) / (1 - 0)"})," ="," ",(0,o.jsx)(h.Z,{children:"-200 / 1"})," ="," ",(0,o.jsx)(h.Z,{children:"-200"}),"."]}),(0,o.jsx)(a.Z.ListItem,{children:"Our b value is the y-axis intercept, which is our initial curved value, 200."}),(0,o.jsx)(a.Z.ListItem,{children:"x will be the ratio of scroll-through, between 0 and 1, that we'll get from our scroll handler."})]}),(0,o.jsx)(s.Z,{children:"Filling it in:"}),(0,o.jsx)(s.Z,{style:{textAlign:"center"},children:(0,o.jsx)(h.Z,{children:"y = -200x + 200"})}),(0,o.jsx)(s.Z,{children:"If it's 25% of the way through, x will be 0.25, and so our y value would be y = (-200)(0.25) + 200 = 150, which is correct: 150 is 1/4 of the way between 200 and 0."}),(0,o.jsx)(s.Z,{children:"Here's our function that performs the above calculations:"}),(0,o.jsx)(q,{includeContentStyles:!0,lang:"js",code:Q}),(0,o.jsxs)(s.Z,{children:["Looks like teenage-me was wrong; algebra ",(0,o.jsx)("strong",{children:"is"})," ","useful and practical!"]}),(0,o.jsx)(f.Z,{size:80}),(0,o.jsx)(l.SectionHeading,{anchorId:"handling-scroll-in-react",children:"Handling Scroll in React"}),(0,o.jsx)(s.Z,{children:"We're in the home stretch now! Time to combine all these ideas into something usable."}),(0,o.jsxs)(s.Z,{children:["Let's start by building a component that contains our scroll-handler to interpolate from the bottom of the viewport to the top, and connect those values to a B\xe9zier curve in the"," ",(0,o.jsx)(m.Z,{children:"render"})," function:"]}),(0,o.jsx)(q,{includeContentStyles:!0,lang:"jsx",code:F}),(0,o.jsx)(s.Z,{children:"This initial approach seems to work OK! There are two things I want to improve though:"}),(0,o.jsxs)(a.Z,{children:[(0,o.jsxs)(a.Z.ListItem,{children:['The "timing" of the flattening feels wrong to me.',(0,o.jsx)("br",{}),(0,o.jsx)(f.Z,{size:10}),"When the curve fully enters the viewport, it's already starting to be flattened. We don't get to see it in 100%-curved form. Worse, it hasn't finished flattening by the time it scrolls out of view! This is because this page has a header that takes up the top 50px of the viewport, and we aren't taking that into account.",(0,o.jsx)("br",{}),(0,o.jsx)(f.Z,{size:10}),"To solve these problems, we need to define a"," ",(0,o.jsx)(m.Z,{children:"scrollable area"}),", instead of using the viewport."]}),(0,o.jsx)(a.Z.ListItem,{children:"This component is doing an awful lot. It feels like we could extract a couple components from this. Refactoring it would not only make it easier to follow/understand, but it would make it more reusable."})]}),(0,o.jsx)(s.Z,{children:"Let's fix these problems. Here's a refactored version:"}),(0,o.jsx)(q,{includeContentStyles:!0,lang:"jsx",code:N}),(0,o.jsxs)(s.Z,{children:["Ahh, much nicer! The effect is more pleasant as the flattening animation happens within a smaller scroll window, and the code is easier to parse. As a bonus, our"," ",(0,o.jsx)(h.Z,{children:"BezierCurve"})," and"," ",(0,o.jsx)(h.Z,{children:"ScrollArea"})," components are generic, so they could be useful in totally different contexts."]}),(0,o.jsx)(f.Z,{size:25}),(0,o.jsx)(l.SectionHeading,{anchorId:"another-note-on-performance",children:"Another note on performance"}),(0,o.jsx)(s.Z,{children:"The two versions above were written without any concern for performance. As it turns out, the performance is not so bad; on my low-end Chromebook, it stutters a little bit from time to time but mostly runs at 60fps. On my sluggish iPhone 6, it runs well enough (the biggest issue on mobile is that the browser address bar changes on scroll. Because of that, it may be wise to disable scroll-based things like this altogether on mobile)."}),(0,o.jsx)(s.Z,{children:"That said, your mileage may vary. If you want to improve performance, there are a few ways this could be optimized:"}),(0,o.jsxs)(a.Z,{children:[(0,o.jsxs)(a.Z.ListItem,{children:[(0,o.jsx)(c.Z,{href:"https://codeburst.io/throttling-and-debouncing-in-javascript-b01cad5c8edf",children:"Throttle"})," ","the scroll-handler in ",(0,o.jsx)(h.Z,{children:"ScrollArea"})," ","that it only fires every 20ms or so. This is to calm down certain touch-screen or trackpad interfaces that can fire far more often than is required."]}),(0,o.jsxs)(a.Z.ListItem,{children:["One of the more expensive parts of this effect is that we're interacting with the DOM, via"," ",(0,o.jsx)(c.Z,{href:"https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect",children:(0,o.jsx)(h.Z,{children:"getBoundingClientRect"})}),", on every scroll event. Ideally, we could cache the position of our ",(0,o.jsx)(h.Z,{children:"ScrollArea"})," on mount, and then check the current scroll distance against this value.",(0,o.jsx)("br",{}),(0,o.jsx)(f.Z,{size:10}),"Unfortunately, this method opens up new problems. It assumes that nothing between the top of the document and your B\xe9zier curve will change height, since our calculations assume a static distance between the two. Mobile browsers like iOS Safari will hide their chrome as you scroll down, so we'd have to factor that in as well.",(0,o.jsx)("br",{}),(0,o.jsx)(f.Z,{size:10}),"It's far from impossible, but it wasn't worth the trouble for me, given that performance was satisfactory on the devices I'm targeting."]}),(0,o.jsxs)(a.Z.ListItem,{children:["By storing",(0,o.jsx)(h.Z,{children:"scrollRatio"})," in state and re-rendering whenever it changes, React needs some time to work out how the DOM has changed as a result of the scroll.",(0,o.jsx)("br",{}),(0,o.jsx)(f.Z,{size:10}),"The refactor to extract several components, while very good for DX and reusability, also means that React has a slightly more complex tree to reconcile.",(0,o.jsx)("br",{}),(0,o.jsx)(f.Z,{size:10}),"This all sounds a bit scary, but as we discovered earlier, React's reconciliation process is very quick on small trees like this. The cost of the refactor was negligible on my chromebook.",(0,o.jsx)("br",{}),(0,o.jsx)(f.Z,{size:10}),"If you really need to extract every drop of performance, you could work with the DOM directly, by setting the new"," ",(0,o.jsx)(h.Z,{children:"path"})," instructions using"," ",(0,o.jsx)(h.Z,{children:"setAttribute"}),". Note that you'd need to store everything in 1 component again."]})]}),(0,o.jsx)(w,{}),(0,o.jsx)(l.SectionHeading,{anchorId:"in-conclusion",children:"In Conclusion"}),(0,o.jsx)(s.Z,{children:"Whew, you made it through this B\xe9zier deep-dive!"}),(0,o.jsx)(s.Z,{children:"The technique described in this blog post is foundational, and there's tons of flourishes you can add on top of it:"}),(0,o.jsxs)(a.Z,{children:[(0,o.jsx)(a.Z.ListItem,{children:"This blog uses 3 layered B\xe9zier curves with different fill colours to provide depth to the experience."}),(0,o.jsxs)(a.Z.ListItem,{children:["You can experiment with different easings for the interpolation (B\xe9zier curves are often used for"," ",(0,o.jsx)(c.Z,{href:"http://cubic-bezier.com/",children:"timing functions"}),", after all!). What if the curve got"," ",(0,o.jsx)("em",{children:"even more dramatic"})," before smoothing it out?"]}),(0,o.jsxs)(a.Z.ListItem,{children:["You could experiment with"," ",(0,o.jsx)(c.Z,{href:"/animation/a-friendly-introduction-to-spring-physics",children:"spring physics"}),", to give the transition inertia."]})]}),(0,o.jsxs)(s.Z,{children:["I'm excited to see what you build with this technique! Let me know"," ",(0,o.jsx)(c.Z,{href:"https://bsky.app/profile/joshwcomeau.com",children:"on Bluesky"}),"."]}),(0,o.jsx)(f.Z,{size:80}),(0,o.jsx)(l.SectionHeading,{children:"Additional Reading"}),(0,o.jsx)(s.Z,{children:"Learn more about the math and mechanics behind B\xe9zier curves with these two amazing resources:"}),(0,o.jsxs)(a.Z,{children:[(0,o.jsxs)(a.Z.ListItem,{children:[(0,o.jsx)(c.Z,{href:"http://jamie-wong.com/post/bezier-curves/",children:"Bezier Curves from the Ground Up"}),", by Jamie Wong"]}),(0,o.jsxs)(a.Z.ListItem,{children:[(0,o.jsx)(c.Z,{href:"https://pomax.github.io/bezierinfo/",children:"A Primer on B\xe9zier curves"}),', by Mike "Pomax" Kamermans']})]})]})},X='\n \n \n \n\n',Y='\n\n \n\n',U='const startPoint = [25, 25];\nconst controlPoint = [300, 175];\nconst endPoint = [25, 325];\n\n\n \n',O='\n \n\n',E="class Bezier extends React.PureComponent {\n constructor(props) {\n super(props);\n\n this.state = {\n // These are our 3 B\xe9zier points, stored in state.\n startPoint: { x: 10, y: 10 },\n controlPoint: { x: 190, y: 100 },\n endPoint: { x: 10, y: 190 },\n\n // We keep track of which point is currently being\n // dragged. By default, no point is.\n draggingPointId: null,\n };\n }\n\n handleMouseDown(pointId) {\n this.setState({ draggingPointId: pointId });\n }\n\n handleMouseUp() {\n this.setState({ draggingPointId: null });\n }\n\n handleMouseMove({ clientX, clientY }) {\n const { viewBoxWidth, viewBoxHeight } = this.props;\n const { draggingPointId } = this.state;\n\n // If we're not currently dragging a point, this is\n // a no-op. Nothing needs to be done.\n if (!draggingPointId) {\n return;\n }\n\n // During render, we capture a reference to the SVG\n // we're drawing, and store it on the instance with\n // `this.node`.\n // If we were to `console.log(this.node)`, we'd see a\n // reference to the underlying HTML element.\n // eg. ` (this.node = node)}\n viewBox={`0 0 ${viewBoxWidth} ${viewBoxHeight}`}\n onMouseMove={ev => this.handleMouseMove(ev)}\n onMouseUp={() => this.handleMouseUp()}\n onMouseLeave={() => this.handleMouseUp()}\n style={{ width: '100%', overflow: 'visible' }}\n >\n \n \n\n \n\n \n this.handleMouseDown('startPoint')\n }\n />\n\n \n this.handleMouseDown('endPoint')\n }\n />\n\n \n this.handleMouseDown('controlPoint')\n }\n />\n \n );\n }\n}\n\n// These helper stateless-functional-components allow us\n// to reuse styles, and give each shape a meaningful name.\n\nconst ConnectingLine = ({ from, to }) => (\n \n);\n\nconst Curve = ({ instructions }) => (\n \n);\n\nconst LargeHandle = ({ coordinates, onMouseDown }) => (\n \n);\n\nconst SmallHandle = ({ coordinates, onMouseDown }) => (\n \n);",Q="/**\n* `getInterpolatedValue` provides a midpoint value\n* between y1 and y2, based on the ratio provided.\n*\n* @param {number} y1 - the value when our curve is\n* totally curvy\n* @param {number} y2 - the value when our curve is\n* totally flat\n* @param {number} x - a value from 0 to 1 that\n* represents the ratio of curvy\n* to flat (0 = totally curvy,\n* 1 = totally flat).\n*/\nconst getInterpolatedValue = (y1, y2, x) => {\n // The slope of a line can be calculated as Δy / Δx.\n //\n // In this case, the domain of our function (AKA the\n // possible X values) are from 0 (x1) to 1 (x2).\n // Δx is therefore just equal to 1 (since 1 - 0 = 1).\n //\n // Because dividing by 1 has no effect, our slope in\n // this case can just be Δy.\n const a = y2 - y1;\n\n // Next, we know that y = ax + b.\n //\n // `b` is the Y-axis intercept, which we know is `y1`,\n // since `y1` is the `y` value when `x` is 0.\n return a * x + y1;\n}\n\n\n// Let's test it! Feel free to sub in your own values\n// to see how the output changes.\n\n// Case 1: `y` ranges from 200 to 0, with 25% through.\nconst case1 = getInterpolatedValue(200, 0, 0.25);\n\n// Case 2: `y` ranges from 0-1000, and we're 90% there.\nconst case2 = getInterpolatedValue(0, 1000, 0.9);\n\n\nrender(\n \n

Test cases:

\n Case 1 is {case1}.
\n Case 2 is {case2}.\n
\n);\n",F="class DynamicBezierCurve extends React.PureComponent {\n constructor(props) {\n super(props);\n\n // As the user scrolls through our scrollable area,\n // the scrollRatio represents the amount completed,\n // from 0 (way at the bottom) to 1 (at the top).\n this.state = {\n scrollRatio: 0,\n };\n\n // This live-editing environment doesn't support\n // property-initializer syntax, so I'm doing my\n // binds in the constructor. \uD83E\uDD37\n this.handleScroll = this.handleScroll.bind(this);\n }\n\n componentDidMount() {\n window.addEventListener('scroll', this.handleScroll);\n }\n\n componentWillUnmount() {\n window.removeEventListener('scroll', this.handleScroll);\n }\n\n handleScroll(ev) {\n const windowHeight = window.innerHeight;\n const svgBB = this.node.getBoundingClientRect();\n\n const pixelsScrolled = windowHeight - svgBB.top;\n\n let scrollRatio = pixelsScrolled / windowHeight;\n\n // We don't care about the negative values when it's\n // below the viewport, or the greater-than-1 values when\n // it's above the viewport.\n scrollRatio = clamp(scrollRatio, 0, 1);\n\n // Small optimization, avoid re-rendering when the\n // SVG isn't in the viewport.\n if (this.state.scrollRatio !== scrollRatio) {\n this.setState({ scrollRatio });\n }\n }\n\n render() {\n const { scrollRatio } = this.state;\n\n // Use our `getInterpolatedValue` function from the\n // previous code snippet to figure out the values for\n // the start point and the control points.\n const startPoint = getInterpolatedValue(\n 300, // curvy value\n 0, // flat value\n scrollRatio\n );\n\n const firstControlPoint = getInterpolatedValue(\n -100, // curvy value\n 0, // flat value\n scrollRatio\n );\n\n const secondControlPoint = getInterpolatedValue(\n 450, // curvy value\n 0, // flat value\n scrollRatio\n );\n\n // Unlike the other 3 points, the `endPoint` is\n // constant, and doesn't need interpolation.\n const endPoint = 0;\n\n // Create the SVG path instructions, using our\n // interpolated values.\n // Unlike previous examples, we want to fill this one\n // in, not just make a stroked line. So we need to add\n // a couple other lines after the curve, to make sure\n // the box fills in correctly.\n const instructions = `\n M 0,${startPoint}\n C 100,${firstControlPoint}\n 200,${secondControlPoint}\n 300,${endPoint}\n L 300,300\n L 0,300\n `;\n\n // NOTE: the instructions created assume a 300x300\n // viewBox. To make this component more flexible, you\n // could set `viewBoxWidth` and `viewBoxHeight`\n // variables as props.\n\n return (\n (this.node = node)}\n viewBox=\"0 0 300 300\"\n >\n \n \n );\n }\n}\n\n// Utility function that clamps a given value to a\n// specific range (inclusive, between min and max).\nconst clamp = (val, min, max) =>\n Math.max(min, Math.min(max, val));\n",N="/**\n* Our first component, `ScrollArea`, tracks its children\n* as they're scrolled through the scrollable area (a\n* specific subset of the viewport).\n*/\nconst scrollAreaPropTypes = {\n // The number of pixels between the top of the viewport,\n // and the top of the scrollable area:\n topBuffer: PropTypes.number.isRequired,\n // The height, in pixels, of the scrollable area:\n areaHeight: PropTypes.number.isRequired,\n // We'll make the `scrollRatio` data available to its\n // children with a `children` render prop:\n children: PropTypes.func.isRequired,\n};\n\nclass ScrollArea extends React.PureComponent {\n constructor(props) {\n super(props);\n\n this.state = {\n scrollRatio: 0,\n };\n\n this.handleScroll = this.handleScroll.bind(this);\n }\n\n componentDidMount() {\n window.addEventListener('scroll', this.handleScroll);\n }\n\n componentWillUnmount() {\n window.removeEventListener('scroll', this.handleScroll);\n }\n\n handleScroll(ev) {\n const { topBuffer, areaHeight } = this.props;\n\n const windowHeight = window.innerHeight;\n const boundingBox = this.node.getBoundingClientRect();\n\n const distanceToTop = boundingBox.top - topBuffer;\n const pixelsScrolled = areaHeight - distanceToTop;\n\n let scrollRatio = pixelsScrolled / areaHeight;\n scrollRatio = clamp(scrollRatio, 0, 1);\n\n if (this.state.scrollRatio !== scrollRatio) {\n this.setState({ scrollRatio });\n }\n }\n\n render() {\n return (\n
this.node = node}>\n {this.props.children(this.state)}\n
\n );\n }\n}\n\nScrollArea.propTypes = scrollAreaPropTypes;\n\n/**\n* This is a simple B\xe9zier curve presentational component.\n*/\nconst BezierCurve = ({\n viewBoxWidth,\n viewBoxHeight,\n startPoint,\n firstControlPoint,\n secondControlPoint,\n endPoint,\n fill = 'hotpink'\n}) => {\n return (\n \n \n \n );\n}\n\n/**\n* The two components above are the building blocks\n* for the effect we want to build. This last component\n* assembles them.\n*\n* It takes the `headerHeight` as a prop, so that we\n* can flatten it at the right moment.\n*/\nconst ScrollBasedBezier = ({ headerHeight }) => (\n \n {({ scrollRatio }) => {\n // Hardcoding these values since this component\n // isn't meant to be reusable.\n const viewBoxWidth = 300;\n const viewBoxHeight = 300;\n\n const startPointY = getInterpolatedValue(\n 300,\n 0,\n scrollRatio\n );\n\n const firstControlPointY = getInterpolatedValue(\n -100,\n 0,\n scrollRatio\n );\n\n const secondControlPointY = getInterpolatedValue(\n 450,\n 0,\n scrollRatio\n );\n\n const endPointY = 0;\n\n return (\n \n );\n }}\n \n);\n\nconst clamp = (val, min, max) =>\n Math.max(min, Math.min(max, val));\n"},16254:e=>{e.exports={ss8jhfh:"ss8jhfh",p1ytwiv5:"p1ytwiv5",e17jydvx:"e17jydvx",vhow9b3:"vhow9b3",izp3bdj:"izp3bdj",cvm5f8k:"cvm5f8k"}},35646:e=>{e.exports={wwtarvq:"wwtarvq"}},3379:e=>{e.exports={wdv8tbs:"wdv8tbs"}},70489:e=>{e.exports={s1glbvt1:"s1glbvt1"}},49080:e=>{e.exports={sv24af8:"sv24af8"}}}]); //# sourceMappingURL=16234.8d12669e52aafdd9.js.map