From 85d4bb692c35fabff25cfe20ecb97d3e75f5cd28 Mon Sep 17 00:00:00 2001 From: Gabriel Harel Date: Thu, 28 Sep 2023 11:42:20 -0500 Subject: [PATCH] add gooey section + regenerate demo page --- demo/content.ts | 119 +++++++++++++++++++++++++++++++++--------------- index.html | 42 +++++++++-------- 2 files changed, 106 insertions(+), 55 deletions(-) diff --git a/demo/content.ts b/demo/content.ts index 9360ef3..ed9003d 100644 --- a/demo/content.ts +++ b/demo/content.ts @@ -219,7 +219,7 @@ addCanvas( const b = wobbleHandle( frameTime, period / 2 + (rb * period) / 2, - point(width * 0.8, height * 0.5, -90, 150, 90, 150), + point(width * 0.8, height * 0.5, -90, 100, 90, 100), true, ); const c = wobbleHandle( @@ -231,7 +231,7 @@ addCanvas( const d = wobbleHandle( frameTime, period / 2 + (rd * period) / 2, - point(width * 0.2, height * 0.5, 90, 150, -90, 150), + point(width * 0.2, height * 0.5, 90, 100, -90, 100), true, ); @@ -288,7 +288,7 @@ addCanvas(2, (ctx, width, height, animate) => { drawOpen(ctx, start, end, false); }); - return `Curves are drawn by the rendering software using the four input points. By connecting + return `Curves are rendered using the four input points (ends + handles). By connecting points a0-a3 with a line and then splitting each line by the same percentage, we've reduced the number of points by one. Repeating the same process with the new set of points until there is only one point remaining (d0) produces a single point on the line. Repeating this @@ -440,8 +440,8 @@ addCanvas( }); return `The angle of the handles for each point is parallel with the imaginary line - stretching between its neighbors. Technically, a polygon's points have zero - length handles, but the angle can still be calculated.`; + stretching between its neighbors. Even when they have length zero, the angle of the + handles can still be calculated.`; }, (ctx, width, height, animate) => { const period = Math.PI * 1500; @@ -483,7 +483,7 @@ addCanvas( drawClosed(ctx, animatedBlob, true); }); - return `The blob is then made smooth by extending the handles. The exact length becomes + return `The blob is then made smooth by extending the handles. The exact length depends on the distance between the given point and it's next neighbor. This value is multiplied by a ratio that would roughly produce a circle if the points had not been randomly moved.`; @@ -775,7 +775,10 @@ addCanvas( }); return `Points can also be reversed without visually affecting the shape. Then, again can - be shifted all around. In total there are 2 * num_points different orderings of the + be shifted all around. Although reversed ordering doesn't change the shape, it has a + dramatic effect on the animation as it makes the loop flip over itself. +

+ In total there are 2 * num_points different orderings of the points that can work for transition purposes.`; }, ); @@ -845,8 +848,8 @@ addCanvas( (ctx.canvas as any).animationID = animationID; return `The added points can be removed at the end of a transition when the target shape has - been reached. However if the animation is interrupted during interpolation there is no - opportunity to clean up the extra points.`; + been reached. However, if the animation is interrupted during interpolation there is no + opportunity to clean up the extra points.`; }, (ctx, width, height, animate) => { const center: Coord = {x: width * 0.5, y: height * 0.5}; @@ -916,38 +919,80 @@ addCanvas( }); return `Putting all these pieces together, the blob transition library can also be used to - tween between non-blob shapes. The more detail a shape has, the more unconvincing the - animation will look. In these cases, manually creating in-between frames can be a helpful - tool.`; + tween between non-blob shapes. The more detail a shape has, the more unconvincing the + animation will look. In these cases, manually creating in-between frames can be a + helpful tool.`; }, ); -addCanvas(1.8, (ctx, width, height, animate) => { - const size = Math.min(width, height) * 0.8; - const center: Coord = {x: width * 0.5, y: height * 0.5}; +addTitle(4, "Gooeyness"); - const animation = canvasPath(); +addCanvas( + 1.3, + (ctx, width, height, animate) => { + const size = Math.min(width, height) * 0.8; + const center: Coord = {x: (width - size) * 0.5, y: (height - size) * 0.5}; - wigglePreset( - animation, - { - extraPoints: 2, - randomness: 2, - seed: Math.random(), - size, - }, - { - offsetX: center.x - size / 2, - offsetY: center.y - size / 2, - }, - { - speed: 2, - }, - ); + const animation = canvasPath(); - animate(() => { - drawClosed(ctx, animation.renderPoints(), true); - }); + const genFrame = (duration: number) => { + animation.transition({ + duration: duration, + blobOptions: { + extraPoints: 2, + randomness: 3, + seed: Math.random(), + size, + }, + callback: () => genFrame(3000), + timingFunction: "ease", + canvasOptions: {offsetX: center.x, offsetY: center.y}, + }); + }; + genFrame(0); - return `TODO`; -}); + animate(() => { + drawClosed(ctx, animation.renderPoints(), true); + }); + + return `This library uses the keyframe model to define animations. This is a flexible + approach, but it does not lend itself well to the kind of gooey blob shapes invite. +

+ When looking at this animation, you may be able to notice the rhythm of the + keyframes where the points start moving and stop moving at the same time.`; + }, + (ctx, width, height, animate) => { + const size = Math.min(width, height) * 0.8; + const center: Coord = {x: width * 0.5, y: height * 0.5}; + + const animation = canvasPath(); + + wigglePreset( + animation, + { + extraPoints: 2, + randomness: 3, + seed: Math.random(), + size, + }, + { + offsetX: center.x - size / 2, + offsetY: center.y - size / 2, + }, + { + speed: 2, + }, + ); + + animate(() => { + drawClosed(ctx, animation.renderPoints(), true); + }); + + return `In addition to the keyframe API, there is now also pre-built preset which produces a + gooey animation without much effort and much prettier results. +

+ This approach uses a noise field instead of random numbers to move individual points + around continuously and independently. Repeated calls to a noise-field-powered random + number generator will produce self-similar results.`; + }, +); diff --git a/index.html b/index.html index 7d4b600..e03acec 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ -
How it works
\ No newline at end of file +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.statefulAnimationGenerator=void 0;var e=require("./frames"),r=function(r,t,n){return function(a){var i=[],o={},s={},u=0,c=0,m=function(){return a()-c},f=function(){return 0!==u},d=function(){f()&&(c+=m()-u,u=0)},l=function(){f()||(u=m())},F=function(){var r=(0,e.renderFramesAt)({renderCache:o,timestamp:f()?u:m(),currentFrames:i});return o=r.renderCache,r.lastFrameId&&s[r.lastFrameId]&&(setTimeout(s[r.lastFrameId]),delete s[r.lastFrameId]),r.points};return{renderFrame:function(){return t(F())},renderPoints:F,transition:function(){for(var t=[],a=0;a
\n Note there is no constant relationship between the\n percentage that "drew" the point and the arc lengths before/after it. Uniform motion along\n the curve can only be approximated.'}),(0,e.addTitle)(4,"Making a blob"),(0,e.addCanvas)(1.3,function(a,o,i,r){var s={x:.5*o,y:.5*i},l=.3*o;return r(function(o){var i=9+3*Math.sin(o/2e3),r=h(i,l,s);(0,n.tempStyles)(a,function(){a.fillStyle=e.colors.secondary,a.strokeStyle=e.colors.secondary},function(){(0,n.drawPoint)(a,s,2),(0,t.forPoints)(r,function(e){var t=e.curr;(0,n.drawLine)(a,s,t,1,2)})}),(0,n.drawClosed)(a,r,!1)}),"Points are first distributed evenly around the center. At this stage the points\n technically have handles, but since they have a length of zero, they have no effect on\n the shape and it looks like a polygon."},function(i,r,s,l){var c=1500*Math.PI,d={x:.5*r,y:.5*s},u=.3*r,f=Math.random(),p=h(5,u,d);return l(function(r){var s=(0,n.calcBouncePercentage)(c,a.timingFunctions.ease,r),l=(0,o.rand)(f+Math.floor(r/c)+"");(0,n.tempStyles)(i,function(){i.fillStyle=e.colors.secondary,i.strokeStyle=e.colors.secondary},function(){(0,n.drawPoint)(i,d,2),(0,t.forPoints)(p,function(e){var t=e.curr,a=e.next;(0,n.drawLine)(i,t,a(),1,2)})});var h=p.map(function(e){var n=s*(.5*l()-.25);return(0,t.coordPoint)((0,t.splitLine)(n,e,d))});(0,n.drawClosed)(i,h,!0)}),'Points are then randomly moved further or closer to the center. Using a seeded\n random number generator allows repeatable "randomness" whenever the blob is generated\n at a different time or place.'}),(0,e.addCanvas)(1.3,function(a,o,i,r){var s={x:.5*o,y:.5*i},l=u({extraPoints:2,randomness:6,seed:"random",size:.7*o},s),c=(0,t.mapPoints)(l,function(e){var n=e.curr;return n.handleIn.length=150,n.handleOut.length=150,n}),d=l.map(t.coordPoint),h=d.length;return r(function(o){var i=Math.floor(o/2e3)%h,r=Math.abs(Math.sin(o*Math.PI/2e3));(0,n.tempStyles)(a,function(){a.strokeStyle=e.colors.secondary,a.globalAlpha=r},function(){(0,t.forPoints)(d,function(e){var t=e.prev,o=e.next;e.index===i&&(0,n.drawLine)(a,t(),o(),1,2)}),(0,t.forPoints)(c,function(e){var t=e.curr;e.index===i&&(0,n.drawHandles)(a,t,1)})}),(0,n.tempStyles)(a,function(){a.fillStyle=e.colors.secondary},function(){(0,n.drawPoint)(a,s,2)}),(0,n.drawClosed)(a,d,!1)}),"The angle of the handles for each point is parallel with the imaginary line\n stretching between its neighbors. Even when they have length zero, the angle of the\n handles can still be calculated."},function(o,i,r,s){var l=1500*Math.PI,c={x:.5*i,y:.5*r},d=u({extraPoints:2,randomness:6,seed:"random",size:.7*i},c);return s(function(i){var r=(0,n.calcBouncePercentage)(l,a.timingFunctions.ease,i);(0,n.tempStyles)(o,function(){o.fillStyle=e.colors.secondary,o.strokeStyle=e.colors.secondary},function(){(0,n.drawPoint)(o,c,2),(0,t.forPoints)(d,function(e){var t=e.curr,a=e.next;(0,n.drawLine)(o,t,a(),1,2)})});var s=(0,t.mapPoints)(d,function(e){var n=e.curr;return n.handleIn.length*=r,n.handleOut.length*=r,n});(0,n.drawClosed)(o,s,!0)}),"The blob is then made smooth by extending the handles. The exact length\n depends on the distance between the given point and it's next neighbor. This value is\n multiplied by a ratio that would roughly produce a circle if the points had not been\n randomly moved."}),(0,e.addTitle)(4,"Interpolating between blobs"),(0,e.addCanvas)(2,function(o,i,s,l){var c=1e3*Math.PI,d={x:.5*i,y:.5*s},h=u({extraPoints:3,randomness:6,seed:"12345",size:.8*s},d),f=u({extraPoints:3,randomness:6,seed:"abc",size:.8*s},d);return l(function(i){var s=(0,n.calcBouncePercentage)(c,a.timingFunctions.ease,i),l=i+.05*c,d=(0,n.calcBouncePercentage)(c,a.timingFunctions.ease,l),u=(0,t.mod)(l,c)/c;(0,n.forceStyles)(o,function(){var t=(0,e.sizes)().pt;o.fillStyle="transparent",o.lineWidth=t,o.strokeStyle=e.colors.secondary,o.setLineDash([2*t]),u>.5?(o.globalAlpha=.2+10*(1-d),(0,n.drawClosed)(o,h,!1),o.globalAlpha=.2,(0,n.drawClosed)(o,f,!1)):(o.globalAlpha=.2+10*d,(0,n.drawClosed)(o,f,!1),o.globalAlpha=.2,(0,n.drawClosed)(o,h,!1))}),(0,n.drawClosed)(o,(0,r.interpolateBetween)(s,h,f),!0)}),"The simplest way to interpolate between blobs would be to move points 0-N from their\n position in the start blob to their position in the end blob. The problem with this approach\n is that it doesn't allow for all blob to map to all blobs. Specifically it would only be\n possible to animate between blobs that have the same number of points. This means something\n more generic is required."}),(0,e.addCanvas)(1.3,function(a,o,i,r){var l={x:.5*o,y:.5*i},c=7*Math.PI*300,d=(0,e.sizes)().pt,h=u({extraPoints:0,randomness:6,seed:"flip",size:.9*i},l);return r(function(o){var i=(0,t.mod)(o,c)/c,r=Math.floor(8*i);(0,n.drawClosed)(a,(0,s.divide)(r+h.length,h),!0),(0,t.forPoints)(h,function(t){var o=t.curr;a.beginPath(),a.arc(o.x,o.y,6*d,0,2*Math.PI),(0,n.tempStyles)(a,function(){a.strokeStyle=e.colors.secondary,a.lineWidth=d},function(){a.stroke()})})}),"The first step to prepare animation is to make the number of points between the\n start and end shapes equal. This is done by adding points to the shape with least points\n until they are both equal.\n

\n For best animation quality it is important that these points are as evenly distributed\n as possible all around the shape so this is not a recursive algorithm."},function(t,o,i,r){var s=1e3*Math.pow(Math.PI,Math.E),l=(0,n.point)(.1*o,.6*i,0,0,-45,.5*o),c=(0,n.point)(.9*o,.6*i,160,.3*o,0,0);return r(function(o){var i=(0,n.calcBouncePercentage)(s,a.timingFunctions.ease,o),r=f(i,l,c);(0,n.tempStyles)(t,function(){t.fillStyle=e.colors.secondary,t.strokeStyle=e.colors.secondary},function(){(0,n.drawLine)(t,r.a0,r.a1,1),(0,n.drawLine)(t,r.a1,r.a2,1,2),(0,n.drawLine)(t,r.a2,r.a3,1),(0,n.drawLine)(t,r.b0,r.b1,1,2),(0,n.drawLine)(t,r.b1,r.b2,1,2),(0,n.drawPoint)(t,r.a0,1.3,"a0"),(0,n.drawPoint)(t,r.a1,1.3,"a1"),(0,n.drawPoint)(t,r.a2,1.3,"a2"),(0,n.drawPoint)(t,r.a3,1.3,"a3"),(0,n.drawPoint)(t,r.b1,1.3,"b1")}),(0,n.forceStyles)(t,function(){var a=(0,e.sizes)().pt;t.fillStyle=e.colors.secondary,t.strokeStyle=e.colors.secondary,t.lineWidth=a,(0,n.drawOpen)(t,l,c,!1)}),(0,n.tempStyles)(t,function(){t.fillStyle=e.colors.highlight,t.strokeStyle=e.colors.highlight},function(){(0,n.drawLine)(t,r.c0,r.c1,1),(0,n.drawLine)(t,r.a0,r.b0,1),(0,n.drawLine)(t,r.a3,r.b2,1),(0,n.drawPoint)(t,r.b0,1.3,"b0"),(0,n.drawPoint)(t,r.b2,1.3,"b2"),(0,n.drawPoint)(t,r.c0,1.3,"c0"),(0,n.drawPoint)(t,r.c1,1.3,"c1")}),(0,n.tempStyles)(t,function(){return t.fillStyle=e.colors.highlight},function(){return(0,n.drawPoint)(t,r.d0,1.3,"d0")})}),'It is only possible to reliably add points to a blob because attempting to\n remove points without modifying the shape is almost never possible and is expensive to\n compute.\n

\n Adding a point is done using the line-drawing geometry. In this example "d0" is the new\n point with its handles being "c0" and "c1". The original points get new handles "b0" and\n "b2"'}),(0,e.addCanvas)(1.3,function(o,i,s,l){var c=Math.E/Math.PI*1e3,d={x:.5*i,y:.5*s},h=u({extraPoints:3,randomness:6,seed:"shift",size:.9*s},d),f=(0,t.shift)(1,h),p=0,m=0;return l(function(i){var s=(0,t.mod)(i,c),l=a.timingFunctions.ease((0,t.mod)(s,c)/c);l
\n In total there are 2 * num_points different orderings of the\n points that can work for transition purposes."}),(0,e.addCanvas)(1.3,function(e,a,o){var r=Math.random(),s=function(){return e.canvas.animationID!==r},c=1e3*Math.PI,h=.5*a,u=.5*o,f=.8*Math.min(a,o),p=(0,l.statefulAnimationGenerator)(function(e){return(0,t.mapPoints)((0,i.genFromOptions)(e.blobOptions),function(e){var n=e.curr;return n.x+=h-f/2,n.y+=u-f/2,n})},function(t){return(0,n.drawClosed)(e,t,!0)},function(){})(Date.now);requestAnimationFrame(function n(){s()||(e.clearRect(0,0,a,o),p.renderFrame(),requestAnimationFrame(n))});var m=function(){s()||p.transition(g())},b=-1,g=function(e){return void 0===e&&(e={}),b++,d({duration:c,timingFunction:"ease",callback:m,blobOptions:{extraPoints:Math.max(0,(0,t.mod)(b,4)-1),randomness:4,seed:Math.random(),size:f}},e)};return p.transition(g({duration:0})),e.canvas.onclick=function(){s()||p.playPause()},e.canvas.animationID=r,"The added points can be removed at the end of a transition when the target shape has\n been reached. However, if the animation is interrupted during interpolation there is no\n opportunity to clean up the extra points."},function(e,t,a,o){var r=.5*t,s=.5*a,l=.8*Math.min(t,a),d=function(e,n,t){for(var a=2*e,o=2*Math.PI/a,i=[],l=0;l
\n When looking at this animation, you may be able to notice the rhythm of the\n keyframes where the points start moving and stop moving at the same time."},function(e,t,a,o){var i=.8*Math.min(t,a),r=.5*t,s=.5*a,l=(0,c.canvasPath)();return(0,c.wigglePreset)(l,{extraPoints:2,randomness:3,seed:Math.random(),size:i},{offsetX:r-i/2,offsetY:s-i/2},{speed:2}),o(function(){(0,n.drawClosed)(e,l.renderPoints(),!0)}),"In addition to the keyframe API, there is now also pre-built preset which produces a\n gooey animation without much effort and much prettier results.\n

\n This approach uses a noise field instead of random numbers to move individual points\n around continuously and independently. Repeated calls to a noise-field-powered random\n number generator will produce self-similar results."}); +},{"./internal/layout":"rSMP","./internal/canvas":"PBVq","../internal/util":"NSCe","../internal/animate/timing":"SjCR","../internal/rand":"BWRk","../internal/gen":"BJ3L","../internal/animate/interpolate":"/Sl0","../internal/animate/prepare":"F/j+","../internal/animate/state":"+LE9","../public/animate":"+HZB"}]},{},["hNRT"], null) +//# sourceMappingURL=/content.a90eb1ee.js.map \ No newline at end of file