bezier-spline-c.js

total 0
used 0
limit 0
/* --- title: Bezier category: curve files: ../point_src/math.js head point pointlist dragging mouse stage dragging stroke ../point_src/keyboard.js ../point_src/split.js ../point_src/curve-extras.js ../point_src/pointlock.js ../point_src/widgets.js ../point_src/random.js In this example the dirty flag is used for curve tips. */ class BezierPoint extends Point { get handles() { if(this._handles) { return this._handles } /* Install the point, ensuring it has handles. */ let handles = this.createHandles(this) this._handles = handles return handles } createHandles(p) { let pair = p.split(2, 0, Math.PI * .5) pair[0].recordedDistance = pair[0].distance2D(p) pair[0].relRads = degToRad(calculateAngle360(p, pair[0])) pair[1].recordedDistance = pair[1].distance2D(p) pair[1].relRads = degToRad(calculateAngle360(p, pair[1])) pair[0]._other = pair[1] pair[1]._other = pair[0] return pair; } } function getSplineCache(pointList) { let cached = pointList._splineCache if (cached && cached.sources.length == pointList.length && cached.sources.every((p, i) => p === pointList[i])) { return cached } let points = pointList.length ? [{ x: pointList[0].x, y: pointList[0].y, distance: 0 }] : [] let segments = [] let distance = 0 const samples = 100 for (let index = 0; index < pointList.length - 1; index++) { let p = pointList[index] let n = pointList[index + 1] let start = distance let startIndex = points.length - 1 let previous = p for (let i = 1; i <= samples; i++) { let sample = get_bezier_point(p, p.handles[0], n.handles[1], n, i / samples) distance += Math.hypot(sample.x - previous.x, sample.y - previous.y) points.push({ ...sample, distance }) previous = sample } segments.push({ start, length: distance - start, startIndex, endIndex: points.length - 1 }) } pointList._splineCache = { sources: Array.from(pointList), points, segments, length: distance } return pointList._splineCache } function splineIndexAtDistance(cache, distance, uniform) { if (!cache.segments.length) return 0 let segment = cache.segments.find(s => distance <= s.start + s.length) || cache.segments[cache.segments.length - 1] let localDistance = Math.max(0, Math.min(segment.length, distance - segment.start)) if (!uniform || segment.length == 0) { return segment.startIndex + (segment.endIndex - segment.startIndex) * localDistance / (segment.length || 1) } let i = segment.startIndex + 1 while (i < segment.endIndex && cache.points[i].distance < distance) i++ let before = cache.points[i - 1].distance let after = cache.points[i].distance return i - 1 + (distance - before) / (after - before || 1) } function drawSplineCache(ctx, cache, from, to) { ctx.beginPath() if (!cache.points.length) return let at = index => { let i = Math.floor(index) let a = cache.points[i] let b = cache.points[Math.min(i + 1, cache.points.length - 1)] let t = index - i return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t } } let first = at(from) ctx.moveTo(first.x, first.y) if (from < to) { for (let i = Math.floor(from) + 1; i < to; i++) { ctx.lineTo(cache.points[i].x, cache.points[i].y) } } else { for (let i = Math.ceil(from) - 1; i > to; i--) { ctx.lineTo(cache.points[i].x, cache.points[i].y) } } let last = at(to) ctx.lineTo(last.x, last.y) } function startSplineAnimation(pointList, options, { reverse = false, erase = false } = {}) { let { rate = 1, uniform = false } = options if (!Number.isFinite(rate) || rate <= 0) { throw new RangeError('rate must be a positive number') } getSplineCache(pointList) pointList._splineAnimation = { rate, uniform, reverse, erase, startedAt: performance.now() } } function inkSplines(pointList, options = {}) { startSplineAnimation(pointList, options) } function unInkSplines(pointList, options = {}) { startSplineAnimation(pointList, options, { erase: true }) } function inkSplineReverse(pointList, options = {}) { startSplineAnimation(pointList, options, { reverse: true }) } class BezierSpline extends PointList { /* A _spline_ is many line segments presented as a single continuious line. In this example the Spline has special properties. Each point has a handles - applied when the point is pushed. perform `push` then `update` for an append. Else 'add' */ showHandles = true showPoints = true // showMinimal = true mirrorHandles = true add(point) { this.push(new BezierPoint(point)) } render(ctx, extras = {}) { this.checkUpdates(extras) let showHandles = (extras.showHandles == undefined) ? this.showHandles: extras.showHandles let showPoints = (extras.showPoints == undefined) ? this.showPoints: extras.showPoints if (showHandles) { this.renderHandlebars(ctx) this.renderHandles(ctx) } this.renderCurve(ctx, extras) if (showPoints) { this.pen.fill(ctx, '#4499DD', 5) // this.pen.indicator(ctx) } } renderHandles(ctx) { this.forEach((p, i, a) => { let hConf = { radius: 4, width: 1 }; if (i == 0) { p.handles[0].pen.fill(ctx, '#444', 4) } else if (i == a.length - 1) { p.handles[1].pen.fill(ctx, '#444', 4) } else { p.handles.pen.fill(ctx, '#444', 4) } }) } renderHandlebars(ctx) { let color = '#666' this.forEach((p, i, a) => { if (i == 0) { p.handles[0].pen.line(ctx, p, color) } else if (i == a.length - 1) { p.handles[1].pen.line(ctx, p, color) } else { p.handles[0].pen.line(ctx, p, color) p.handles[1].pen.line(ctx, p, color) // p.handles.pen.line(ctx, p, {width: 1, color: 'green'}) } }) } renderCurve(ctx, conf={}) { let animation = this._splineAnimation if (animation) { let cache = getSplineCache(this) let distance = Math.min( cache.length, (performance.now() - animation.startedAt) * animation.rate / 1000 ) let from = animation.reverse ? cache.points.length - 1 : 0 let to = splineIndexAtDistance( cache, animation.reverse || animation.erase ? cache.length - distance : distance, animation.uniform ) drawSplineCache( ctx, cache, from, to ) } else { ctx.beginPath() if (this.length) { ctx.moveTo(this[0].x, this[0].y) for (let i = 0; i < this.length - 1; i++) { let p = this[i] let n = this[i + 1] ctx.bezierCurveTo( p.handles[0].x, p.handles[0].y, n.handles[1].x, n.handles[1].y, n.x, n.y ) } } } ctx.strokeStyle = conf.color || 'purple' ctx.lineWidth = 3 ctx.stroke() } checkUpdates(options) { let mirror = this.mirrorHandles let changed = false if (options.altMode !== undefined) { mirror = !options.altMode } this.forEach((p, i) => { if (p.wasDirty) { changed = true this.computeHandles(i, p) } p.handles.forEach(h => { if (h.wasDirty) { changed = true // store let other = h._other // this.constrainHandle(p, other) h.recordedDistance = h.distance2D(p) h.relRads = degToRad(calculateAngle360(p, h)) if (mirror) { this.mirrorHandle(p, h, h.recordedDistance.distance) } } }) }) if (changed) this._splineCache = undefined } mirrorHandle(owner, handle, distance, scaleDistance=false) { let antipose = handle._other let radians = owner.radians + handle.relRads let antiposeDistance = distance if (scaleDistance) { let originalDistance = handle.recordedDistance.distance let ratio = originalDistance == 0 ? 0 : Math.abs(distance) / originalDistance antiposeDistance = antipose.recordedDistance.distance * ratio } antipose.x = owner.x - Math.cos(radians) * antiposeDistance antipose.y = owner.y - Math.sin(radians) * antiposeDistance antipose.recordedDistance = antipose.distance2D(owner) antipose.relRads = degToRad(calculateAngle360(owner, antipose)) antipose.dirty = false } constrainHandle(owner, handle) { let radians = owner.radians + handle.relRads let directionX = Math.cos(radians) let directionY = Math.sin(radians) let offsetX = handle.x - owner.x let offsetY = handle.y - owner.y let distance = offsetX * directionX + offsetY * directionY handle.x = owner.x + directionX * distance handle.y = owner.y + directionY * distance return distance } computeHandles(index, point) { let handles = point.handles handles.forEach(handle => { let radians = point.radians + (handle?.relRads || 0) let distance = handle?.recordedDistance?.distance || point.radius // handle.x = point.x + Math.cos(radians) * distance handle.y = point.y + Math.sin(radians) * distance }) } } addButton('button', { label: "new spline" , onclick(){ stage.generateCurve() } }) class MainStage extends Stage { canvas='playspace' mounted() { this.myKeys = {} this.keyboard.wake() this.splines = [] } generateCurve() { this.splines.push({ color: random.color([0, 360], [30, 90], [60,80]) , spline: this.createCurve() }) } createCurve() { let spline = new BezierSpline( [100, 150, 60, -30] , [200, 340, 30, -30] , [450, 340, 30, -90] , [650, 340, 30, -90] , [650, 440, 30, 40] ).cast(BezierPoint) inkSplines(spline, { rate: 600, uniform: true }) this.dragging.add(...spline) spline.forEach((p) => { this.dragging.add(...p.handles) }) return spline } onKeydown(ev) { // console.log('onKeydown', ev) this.myKeys[ev.key] = 1 } onKeyup(ev) { // console.log('onKeyup', ev) this.myKeys[ev.key] = 0 } draw(ctx){ this.clear(ctx) let altButton = this?.myKeys['Control'] == 1 this.splines.forEach((sConf) => { let s = sConf.spline; s.render(ctx, { altMode: altButton , color: sConf.color , showHandles: sConf.showHandles , showPoints: sConf.showPoints }) }) } } ;stage = MainStage.go();
Run
Meta Data
title Bezier
imports ()
files ('../point_src/math.js', 'head', 'point', 'pointlist', 'dragging', 'mouse', 'stage', 'dragging', 'stroke', '../point_src/keyboard.js', '../point_src/split.js', '../point_src/curve-extras.js', '../point_src/pointlock.js', '../point_src/widgets.js', '../point_src/random.js')
unused_keys ()
unknown_keys ('category',)
category ['curve']
filepath_exists True
path bezier-spline-c.js
filepath bezier-spline-c.js
clean_files ('../point_src/math.js', '../point_src/core/head.js', '../point_src/pointpen.js', '../point_src/compass.js', '../point_src/center.js', '../point_src/point-content.js', '../point_src/pointdraw.js', '../point_src/relative-xy.js', '../point_src/pointcast.js', '../point_src/point.js', '../point_src/pointlistdraw.js', '../point_src/pointlistgradient.js', '../point_src/pointlistshape.js', '../point_src/pointlistgenerator.js', '../point_src/unpack.js', '../point_src/pointlist.js', '../point_src/pointlistpen.js', '../point_src/functions/clamp.js', '../point_src/distances.js', '../point_src/protractor.js', '../point_src/text/beta.js', '../point_src/dragging.js', '../point_src/events.js', '../point_src/automouse.js', '../point_src/stage-hooks.js', '../point_src/stage-resize.js', '../point_src/functions/resolve.js', '../point_src/stage.js', '../point_src/setunset.js', '../point_src/stroke.js', '../point_src/keyboard.js', '../point_src/split.js', '../point_src/curve-extras.js', '../point_src/pointlock.js', '../point_src/widgets.js', '../point_src/random.js')
markdown {'html': '<p>In this example the dirty flag is used for curve tips.</p>', 'content': '---\ntitle: Bezier\ncategory: curve\nfiles:\n ../point_src/math.js\n head\n point\n pointlist\n dragging\n mouse\n stage\n dragging\n stroke\n ../point_src/keyboard.js\n ../point_src/split.js\n ../point_src/curve-extras.js\n ../point_src/pointlock.js\n ../point_src/widgets.js\n ../point_src/random.js\n\nIn this example the dirty flag is used for curve tips.'}