@@ -32,6 +32,15 @@ import {
3232 hasVisibleFill ,
3333 hasVisibleStroke ,
3434} from './utils/hit-test.js' ;
35+ import {
36+ clearHandleComponent ,
37+ setHandleComponent ,
38+ inheritRelative ,
39+ isSegmentCurved ,
40+ splitSubdivisionSegment ,
41+ applyGlobalSmooth ,
42+ applyLocalSmooth ,
43+ } from './utils/path.js' ;
3544
3645// Constants
3746
@@ -824,6 +833,18 @@ export class Path extends Shape {
824833 } ;
825834 }
826835
836+ /**
837+ * @name Two.Path#contains
838+ * @function
839+ * @param {Number } x - x coordinate to hit test against
840+ * @param {Number } y - y coordinate to hit test against
841+ * @param {Object } [options] - Optional options object
842+ * @param {Boolean } [options.ignoreVisibility] - If `true`, hit test against `path.visible = false` shapes
843+ * @param {Number } [options.tolerance] - Padding to hit test against in pixels
844+ * @returns {Boolean }
845+ * @description Check to see if coordinates are within a {@link Two.Path}'s bounding rectangle
846+ * @nota -bene Expects *world-space coordinates* – the same pixel-space you get from the renderer (e.g., mouse `clientX`/`clientY` adjusted for the canvas’s offset and pixel ratio).
847+ */
827848 contains ( x , y , options ) {
828849 const opts = options || { } ;
829850 const ignoreVisibility = opts . ignoreVisibility === true ;
@@ -832,12 +853,15 @@ export class Path extends Shape {
832853 return false ;
833854 }
834855
835- if ( ! ignoreVisibility && typeof this . opacity === 'number' && this . opacity <= 0 ) {
856+ if (
857+ ! ignoreVisibility &&
858+ typeof this . opacity === 'number' &&
859+ this . opacity <= 0
860+ ) {
836861 return false ;
837862 }
838863
839- const tolerance =
840- typeof opts . tolerance === 'number' ? opts . tolerance : 0 ;
864+ const tolerance = typeof opts . tolerance === 'number' ? opts . tolerance : 0 ;
841865
842866 this . _update ( true ) ;
843867
@@ -878,8 +902,7 @@ export class Path extends Shape {
878902 }
879903
880904 if ( strokeTest && segments . length > 0 ) {
881- const linewidth =
882- typeof this . linewidth === 'number' ? this . linewidth : 0 ;
905+ const linewidth = typeof this . linewidth === 'number' ? this . linewidth : 0 ;
883906 if ( linewidth > 0 ) {
884907 const distance = distanceToSegments ( segments , localX , localY ) ;
885908 if ( distance <= linewidth / 2 + tolerance ) {
@@ -1052,80 +1075,208 @@ export class Path extends Shape {
10521075 return this ;
10531076 }
10541077
1078+ /**
1079+ * @name Two.Path#smooth
1080+ * @function
1081+ * @param {Object } [options] - Configuration for smoothing.
1082+ * @param {String } [options.type='continuous'] - Type of smoothing algorithm.
1083+ * @param {Number } [options.from=0] - Index of vertices to start smoothing
1084+ * @param {Number } [options.to=1] - Index of vertices to terminate smoothing
1085+ * @description Adjust vertex handles to generate smooth curves without toggling `automatic`.
1086+ */
1087+ smooth ( options ) {
1088+ const opts = options || { } ;
1089+ const type = opts . type || 'continuous' ;
1090+ const vertices = this . _collection ;
1091+ const length = vertices . length ;
1092+
1093+ if ( length < 2 ) {
1094+ return this ;
1095+ }
1096+
1097+ const closed =
1098+ this . _closed ||
1099+ ( length > 0 &&
1100+ vertices [ length - 1 ] &&
1101+ vertices [ length - 1 ] . command === Commands . close ) ;
1102+
1103+ const resolveIndex = ( value , defaultIndex ) => {
1104+ if ( value === undefined || value === null ) {
1105+ return defaultIndex ;
1106+ }
1107+
1108+ if ( typeof value === 'number' ) {
1109+ if ( closed ) {
1110+ return mod ( value , length ) ;
1111+ }
1112+ let index = value ;
1113+ if ( index < 0 ) {
1114+ index += length ;
1115+ }
1116+ return Math . min ( Math . max ( index , 0 ) , length - 1 ) ;
1117+ }
1118+
1119+ const idx = vertices . indexOf ( value ) ;
1120+ return idx !== - 1 ? idx : defaultIndex ;
1121+ } ;
1122+
1123+ const loop = closed && opts . from === undefined && opts . to === undefined ;
1124+ let from = resolveIndex ( opts . from , 0 ) ;
1125+ let to = resolveIndex ( opts . to , length - 1 ) ;
1126+
1127+ if ( from > to ) {
1128+ if ( closed ) {
1129+ from -= length ;
1130+ } else {
1131+ const temp = from ;
1132+ from = to ;
1133+ to = temp ;
1134+ }
1135+ }
1136+
1137+ const rangeLength = to - from + 1 ;
1138+ for ( let i = 0 ; i < rangeLength ; i += 1 ) {
1139+ const index = mod ( from + i , length ) ;
1140+ const anchor = vertices [ index ] ;
1141+ const isOpenStart = ! closed && index === 0 ;
1142+ if ( anchor . command === Commands . move && ! isOpenStart ) {
1143+ anchor . command = Commands . line ;
1144+ }
1145+ }
1146+
1147+ if ( type === 'continuous' || type === 'asymmetric' ) {
1148+ applyGlobalSmooth (
1149+ vertices ,
1150+ from ,
1151+ to ,
1152+ closed ,
1153+ loop ,
1154+ type === 'asymmetric'
1155+ ) ;
1156+ } else if ( type === 'catmull-rom' || type === 'geometric' ) {
1157+ const range = {
1158+ type,
1159+ factor : opts . factor ,
1160+ } ;
1161+ applyLocalSmooth ( vertices , from , to , closed , loop , range ) ;
1162+ } else {
1163+ throw new Error (
1164+ `Path.smooth does not support type "${ type } ". Try 'continuous', 'asymmetric', 'catmull-rom', or 'geometric'.`
1165+ ) ;
1166+ }
1167+
1168+ this . _automatic = false ;
1169+ this . _flagVertices = true ;
1170+ this . _flagLength = true ;
1171+
1172+ return this ;
1173+ }
1174+
10551175 /**
10561176 * @name Two.Path#subdivide
10571177 * @function
10581178 * @param {Number } limit - How many times to recurse subdivisions.
10591179 * @description Insert a {@link Two.Anchor} at the midpoint between every item in {@link Two.Path#vertices}.
10601180 */
10611181 subdivide ( limit ) {
1062- // TODO: DRYness (function below)
10631182 this . _update ( ) ;
10641183
1065- const last = this . vertices . length - 1 ;
1066- const closed =
1067- this . _closed || this . vertices [ last ] . _command === Commands . close ;
1068- let b = this . vertices [ last ] ;
1069- let points = [ ] ,
1070- verts ;
1071-
1072- _ . each (
1073- this . vertices ,
1074- function ( a , i ) {
1075- if ( i <= 0 && ! closed ) {
1076- b = a ;
1077- return ;
1078- }
1184+ const vertices = this . vertices ;
1185+ const length = vertices . length ;
1186+ if ( length < 2 ) {
1187+ return this ;
1188+ }
10791189
1080- if ( a . command === Commands . move ) {
1081- points . push ( new Anchor ( b . x , b . y ) ) ;
1082- if ( i > 0 ) {
1083- points [ points . length - 1 ] . command = Commands . line ;
1084- }
1085- b = a ;
1086- return ;
1087- }
1190+ const points = [ ] ;
1191+ let prevOriginal = null ;
1192+ let subpathStartOriginal = null ;
10881193
1089- verts = getSubdivisions ( a , b , limit ) ;
1090- points = points . concat ( verts ) ;
1194+ for ( let i = 0 ; i < length ; i += 1 ) {
1195+ const currentOriginal = vertices [ i ] ;
10911196
1092- // Assign commands to all the verts
1093- _ . each ( verts , function ( v , i ) {
1094- if ( i <= 0 && b . command === Commands . move ) {
1095- v . command = Commands . move ;
1096- } else {
1097- v . command = Commands . line ;
1098- }
1099- } ) ;
1197+ if ( ! prevOriginal || currentOriginal . command === Commands . move ) {
1198+ const clone = currentOriginal . clone ( ) ;
1199+ points . push ( clone ) ;
1200+ prevOriginal = currentOriginal ;
1201+ subpathStartOriginal = currentOriginal ;
1202+ continue ;
1203+ }
11001204
1101- if ( i >= last ) {
1102- // TODO: Add check if the two vectors in question are the same values.
1103- if ( this . _closed && this . _automatic ) {
1104- b = a ;
1205+ const isCurve = isSegmentCurved ( currentOriginal , prevOriginal ) ;
11051206
1106- verts = getSubdivisions ( a , b , limit ) ;
1107- points = points . concat ( verts ) ;
1207+ if ( isCurve ) {
1208+ const subdivided = getSubdivisions ( currentOriginal , prevOriginal , limit ) ;
1209+ const steps = subdivided . length ;
1210+ const prevClone = points [ points . length - 1 ] ;
1211+ let startSegment = prevClone . clone ( ) ;
1212+ let endSegment = currentOriginal . clone ( ) ;
1213+ let prevCloneRef = prevClone ;
1214+ let prevT = 0 ;
11081215
1109- // Assign commands to all the verts
1110- _ . each ( verts , function ( v , i ) {
1111- if ( i <= 0 && b . command === Commands . move ) {
1112- v . command = Commands . move ;
1113- } else {
1114- v . command = Commands . line ;
1115- }
1116- } ) ;
1216+ if ( steps <= 1 ) {
1217+ const currentClone = currentOriginal . clone ( ) ;
1218+ points . push ( currentClone ) ;
1219+ } else {
1220+ for ( let j = 1 ; j < steps ; j += 1 ) {
1221+ const globalT = j / steps ;
1222+ const denom = 1 - prevT ;
1223+ const localT =
1224+ denom <= Number . EPSILON ? globalT : ( globalT - prevT ) / denom ;
1225+
1226+ const split = splitSubdivisionSegment (
1227+ startSegment ,
1228+ endSegment ,
1229+ localT
1230+ ) ;
1231+
1232+ setHandleComponent (
1233+ prevCloneRef ,
1234+ 'right' ,
1235+ split . startOut . x - prevCloneRef . x ,
1236+ split . startOut . y - prevCloneRef . y
1237+ ) ;
1238+
1239+ const newAnchor = split . anchor ;
1240+ points . push ( newAnchor ) ;
1241+
1242+ prevCloneRef = newAnchor ;
1243+ startSegment = newAnchor . clone ( ) ;
1244+ prevT = globalT ;
1245+
1246+ setHandleComponent (
1247+ endSegment ,
1248+ 'left' ,
1249+ split . endIn . x - endSegment . x ,
1250+ split . endIn . y - endSegment . y
1251+ ) ;
11171252 }
11181253
1119- points . push ( new Anchor ( a . x , a . y ) ) ;
1120- points [ points . length - 1 ] . command = closed
1121- ? Commands . close
1122- : Commands . line ;
1254+ const currentClone = currentOriginal . clone ( ) ;
1255+ currentClone . controls . left . copy ( endSegment . controls . left ) ;
1256+ points . push ( currentClone ) ;
1257+ }
1258+ } else {
1259+ const subdivided = getSubdivisions ( currentOriginal , prevOriginal , limit ) ;
1260+
1261+ for ( let j = 1 ; j < subdivided . length ; j += 1 ) {
1262+ const anchor = subdivided [ j ] ;
1263+ inheritRelative ( anchor , prevOriginal ) ;
1264+ clearHandleComponent ( anchor , 'left' ) ;
1265+ clearHandleComponent ( anchor , 'right' ) ;
1266+ anchor . command = Commands . line ;
1267+ points . push ( anchor ) ;
11231268 }
11241269
1125- b = a ;
1126- } ,
1127- this
1128- ) ;
1270+ const currentClone = currentOriginal . clone ( ) ;
1271+ points . push ( currentClone ) ;
1272+ }
1273+
1274+ prevOriginal = currentOriginal ;
1275+
1276+ if ( currentOriginal . command === Commands . close ) {
1277+ prevOriginal = subpathStartOriginal ;
1278+ }
1279+ }
11291280
11301281 this . _automatic = false ;
11311282 this . _curved = false ;
0 commit comments