@@ -69,6 +69,22 @@ export const distance = (a: Coordinates, b: Coordinates): number => {
6969 return Math . sqrt ( ( a . x - b . x ) ** 2 + ( a . y - b . y ) ** 2 ) ;
7070} ;
7171
72+ const point = ( x : number , y : number , ia : number , il : number , oa : number , ol : number ) : Point => {
73+ return {
74+ x : x * size ,
75+ y : y * size ,
76+ handleIn : { angle : rad ( ia ) , length : il * size } ,
77+ handleOut : { angle : rad ( oa ) , length : ol * size } ,
78+ } ;
79+ } ;
80+
81+ const copyPoint = ( p : Point ) : Point => ( {
82+ x : p . x ,
83+ y : p . y ,
84+ handleIn : { ...p . handleIn } ,
85+ handleOut : { ...p . handleOut } ,
86+ } ) ;
87+
7288const expandHandle = ( origin : Coordinates , handle : Handle ) : Coordinates => {
7389 return {
7490 x : origin . x + handle . length * Math . cos ( handle . angle ) ,
@@ -132,6 +148,60 @@ const approxCurveLength = (a: Point, b: Point): number => {
132148 return ( ab + abHandle + a . handleOut . length + b . handleIn . length ) / 2 ;
133149} ;
134150
151+ const divideShape = ( count : number , points : Point [ ] ) : Point [ ] => {
152+ if ( points . length < 3 ) throw new Error ( "not enough points" ) ;
153+ if ( count < points . length ) throw new Error ( "cannot remove points" ) ;
154+ if ( count === points . length ) return points . slice ( ) ;
155+
156+ const lengths = [ ] ;
157+ for ( let i = 0 ; i < points . length ; i ++ ) {
158+ lengths . push ( approxCurveLength ( points [ i ] , points [ ( i + 1 ) % points . length ] ) ) ;
159+ }
160+
161+ const divisors = divideLengths ( lengths , count - points . length ) ;
162+ const out : Point [ ] = [ ] ;
163+ for ( let i = 0 ; i < points . length ; i ++ ) {
164+ const curr : Point = out [ out . length - 1 ] || points [ i ] ;
165+ const next = points [ ( i + 1 ) % points . length ] ;
166+ out . pop ( ) ;
167+ out . push ( ...splitCurveBy ( divisors [ i ] , curr , next ) ) ;
168+ }
169+ const last = out . pop ( ) ;
170+ out [ 0 ] . handleIn = last ! . handleIn ;
171+
172+ return out ;
173+ } ;
174+
175+ const divideLengths = ( lengths : number [ ] , add : number ) : number [ ] => {
176+ const divisors = lengths . map ( ( ) => 1 ) ;
177+ const sizes = lengths . slice ( ) ;
178+ for ( let i = 0 ; i < add ; i ++ ) {
179+ let maxSizeIndex = 0 ;
180+ for ( let j = 1 ; j < sizes . length ; j ++ ) {
181+ if ( sizes [ j ] > sizes [ maxSizeIndex ] ) {
182+ maxSizeIndex = j ;
183+ continue ;
184+ }
185+ if ( sizes [ j ] === sizes [ maxSizeIndex ] ) {
186+ if ( lengths [ j ] > lengths [ maxSizeIndex ] ) {
187+ maxSizeIndex = j ;
188+ }
189+ }
190+ }
191+ divisors [ maxSizeIndex ] ++ ;
192+ sizes [ maxSizeIndex ] = lengths [ maxSizeIndex ] / divisors [ maxSizeIndex ] ;
193+ }
194+ return divisors ;
195+ } ;
196+
197+ const splitCurveBy = ( count : number , a : Point , b : Point ) : Point [ ] => {
198+ if ( count < 2 ) return [ a , b ] ;
199+ const percentage = 1 / count ;
200+ const [ c , d , e ] = splitCurveAt ( percentage , a , b ) ;
201+ if ( count === 2 ) return [ c , d , e ] ;
202+ return [ c , ...splitCurveBy ( count - 1 , d , e ) ] ;
203+ } ;
204+
135205// Add a control point to the curve between a and b.
136206// Percentage [0, 1] from a to b.
137207// a: original first point.
@@ -142,31 +212,12 @@ const approxCurveLength = (a: Point, b: Point): number => {
142212// f: split point between a and b's handles.
143213// g: split point between c's handle and f.
144214// h: split point between e's handle and f.
145- const splitCurve = ( percentage : number , a : Point , b : Point ) : [ Point , Point , Point ] => {
146- const c : Point = {
147- x : a . x ,
148- y : a . y ,
149- handleIn : {
150- angle : a . handleIn . angle ,
151- length : a . handleIn . length ,
152- } ,
153- handleOut : {
154- angle : a . handleOut . angle ,
155- length : a . handleOut . length * percentage ,
156- } ,
157- } ;
158- const e : Point = {
159- x : b . x ,
160- y : b . y ,
161- handleIn : {
162- angle : b . handleIn . angle ,
163- length : b . handleIn . length * ( 1 - percentage ) ,
164- } ,
165- handleOut : {
166- angle : b . handleOut . angle ,
167- length : b . handleOut . length ,
168- } ,
169- } ;
215+ const splitCurveAt = ( percentage : number , a : Point , b : Point ) : [ Point , Point , Point ] => {
216+ const c = copyPoint ( a ) ;
217+ c . handleOut . length *= percentage ;
218+
219+ const e = copyPoint ( b ) ;
220+ e . handleIn . length *= 1 - percentage ;
170221
171222 const aHandle = expandHandle ( a , a . handleOut ) ;
172223 const bHandle = expandHandle ( b , b . handleIn ) ;
@@ -200,8 +251,8 @@ const splitCurve = (percentage: number, a: Point, b: Point): [Point, Point, Poin
200251 return [ c , d , e ] ;
201252} ;
202253
203- const render = ( points : Point [ ] ) => {
204- if ( points . length < 3 ) throw new Error ( "not enough points" ) ;
254+ const renderShape = ( points : Point [ ] ) => {
255+ if ( points . length < 2 ) throw new Error ( "not enough points" ) ;
205256
206257 // Draw points.
207258 for ( let i = 0 ; i < points . length ; i ++ ) {
@@ -225,64 +276,20 @@ const render = (points: Point[]) => {
225276 }
226277} ;
227278
228- const renderTestShape = ( percentage : number ) => {
279+ const testSplitAt = ( percentage : number ) => {
229280 let points : Point [ ] = [
230- {
231- x : 0.2 * size ,
232- y : 0.2 * size ,
233- handleIn : {
234- angle : rad ( 135 ) ,
235- length : 0.1 * size ,
236- } ,
237- handleOut : {
238- angle : rad ( 315 ) ,
239- length : 0.2 * size ,
240- } ,
241- } ,
242- {
243- x : 0.8 * size ,
244- y : 0.2 * size ,
245- handleIn : {
246- angle : rad ( 225 ) ,
247- length : 0.1 * size ,
248- } ,
249- handleOut : {
250- angle : rad ( 45 ) ,
251- length : 0.2 * size ,
252- } ,
253- } ,
254- {
255- x : 0.8 * size ,
256- y : 0.8 * size ,
257- handleIn : {
258- angle : rad ( 315 ) ,
259- length : 0.1 * size ,
260- } ,
261- handleOut : {
262- angle : rad ( 135 ) ,
263- length : 0.2 * size ,
264- } ,
265- } ,
266- {
267- x : 0.2 * size ,
268- y : 0.8 * size ,
269- handleIn : {
270- angle : rad ( 45 ) ,
271- length : 0.1 * size ,
272- } ,
273- handleOut : {
274- angle : rad ( 225 ) ,
275- length : 0.2 * size ,
276- } ,
277- } ,
281+ point ( 0.15 , 0.15 , 135 , 0.1 , 315 , 0.2 ) ,
282+ point ( 0.85 , 0.15 , 225 , 0.1 , 45 , 0.2 ) ,
283+ point ( 0.85 , 0.85 , 315 , 0.1 , 135 , 0.2 ) ,
284+ point ( 0.15 , 0.85 , 45 , 0.1 , 225 , 0.2 ) ,
278285 ] ;
279286
280287 const count = points . length ;
281288 const stop = 2 * count - 1 ;
282289 for ( let i = 0 ; i < count ; i ++ ) {
283290 const double = i * 2 ;
284291 const next = ( double + 1 ) % stop ;
285- points . splice ( double , 2 , ...splitCurve ( percentage , points [ double ] , points [ next ] ) ) ;
292+ points . splice ( double , 2 , ...splitCurveAt ( percentage , points [ double ] , points [ next ] ) ) ;
286293 }
287294 points . splice ( 0 , 1 ) ;
288295
@@ -292,29 +299,35 @@ const renderTestShape = (percentage: number) => {
292299 const next = points [ ( i + 1 ) % points . length ] ;
293300 length += approxCurveLength ( curr , next ) ;
294301 }
295- drawInfo ( "shape path lengths sum" , length ) ;
302+ drawInfo ( "split at lengths sum" , length ) ;
303+
304+ renderShape ( points ) ;
305+ } ;
296306
297- render ( points ) ;
307+ const testSplitBy = ( ) => {
308+ const count = 10 ;
309+ for ( let i = 0 ; i < count ; i ++ ) {
310+ renderShape (
311+ splitCurveBy (
312+ i + 1 ,
313+ point ( 0.15 , 0.2 + i * 0.06 , 30 , 0.1 , - 30 , 0.1 ) ,
314+ point ( 0.45 , 0.2 + i * 0.06 , 135 , 0.1 , 225 , 0.1 ) ,
315+ ) ,
316+ ) ;
317+ }
298318} ;
299319
300- const renderTestCurve = ( percentage : number ) => {
301- render (
302- splitCurve (
303- percentage ,
304- {
305- x : 0.3 * size ,
306- y : 0.3 * size ,
307- handleIn : { angle : 0 , length : 0 } ,
308- handleOut : { angle : 0 , length : 0.4 * size } ,
309- } ,
310- {
311- x : 0.7 * size ,
312- y : 0.7 * size ,
313- handleIn : { angle : Math . PI , length : 0.4 * size } ,
314- handleOut : { angle : 0 , length : 0 } ,
315- } ,
316- ) ,
317- ) ;
320+ const testDivideShape = ( ) => {
321+ const count = 10 ;
322+ for ( let i = 0 ; i < count ; i ++ ) {
323+ renderShape (
324+ divideShape ( i + 3 , [
325+ point ( 0.6 , 0.2 + i * 0.05 , - 10 , 0.1 , - 45 , 0.03 ) ,
326+ point ( 0.7 , 0.2 + i * 0.05 - 0.03 , 180 , 0.03 , 0 , 0.03 ) ,
327+ point ( 0.8 , 0.2 + i * 0.05 , - 135 , 0.03 , 170 , 0.1 ) ,
328+ ] ) ,
329+ ) ;
330+ }
318331} ;
319332
320333( ( ) => {
@@ -331,8 +344,9 @@ const renderTestCurve = (percentage: number) => {
331344 const renderFrame = ( ) => {
332345 ctx . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
333346 drawInfo ( "percentage" , percentage ) ;
334- renderTestCurve ( percentage ) ;
335- renderTestShape ( percentage ) ;
347+ testSplitAt ( percentage ) ;
348+ testSplitBy ( ) ;
349+ testDivideShape ( ) ;
336350 percentage += animationSpeed / 1000 ;
337351 percentage %= 1 ;
338352 if ( animationSpeed > 0 ) requestAnimationFrame ( renderFrame ) ;
0 commit comments