Last active
March 31, 2023 02:04
-
-
Save moonraker22/c4e592ebb70463a5c3652d3353a06f59 to your computer and use it in GitHub Desktop.
transform an svg to a path in three js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // FROM https://gist.github.com/IkarosKappler/d3c39db08115085bcb18#file-fix_by_ikaros-js | |
| // used in https://codepen.io/loficodes/pen/MWENJxL/d06de09715a270406e86f5cbc0b3c16e?editors=1111 | |
| // usage | |
| loader.load("https://assets.codepen.io/5946/fish_1.glb", function (gltf) { | |
| fish = gltf.scene; | |
| const svg = document.getElementById(svgName); | |
| const origPoints = getCenteredSVGPoints(svg, 0.025); | |
| const fishPoints = getFishPointsFromPoints(origPoints); | |
| showLineFromPoints(origPoints, 0x00ff00); | |
| showLineFromPoints(fishPoints, 0xff0000); | |
| const fishCurve = new THREE.CatmullRomCurve3(fishPoints, true); | |
| followPoints(fishCurve); | |
| animate(); | |
| }); | |
| const getCenteredSVGPoints = (svg, scale) => { | |
| const viewBox = svg.getAttribute("viewBox").split(" "); | |
| const width = parseFloat(viewBox[2]); | |
| const height = parseFloat(viewBox[3]); | |
| const path = svg.querySelector("path").getAttribute("d"); | |
| const shape = transformSVGPath(path); | |
| const points = shape.getPoints(pointCount).map((point) => { | |
| let v = new THREE.Vector3(point.x - width / 2, 0, point.y - height / 2); | |
| v = v.multiplyScalar(scale); | |
| return v; | |
| }); | |
| return points; | |
| }; | |
| const getFishPointsFromPoints = (points) => { | |
| const fishPoints = []; | |
| console.log(frequency, amplitude); | |
| const curve = new THREE.CatmullRomCurve3(points); | |
| for (let i = 0; i < pointCount; i++) { | |
| const t = i / pointCount; | |
| const angle = (i / (pointCount / frequency)) % 1; | |
| const displacement = Math.sin(Math.PI * 2 * angle) * amplitude; | |
| let point = curve.getPoint(t); | |
| const tangeant = curve.getTangent(t); | |
| const normal = tangeant.clone().cross(new THREE.Vector3(0, 1, 0)); | |
| point = point.add(normal.multiplyScalar(displacement)); | |
| fishPoints.push(point); | |
| } | |
| return fishPoints; | |
| }; | |
| const followPoints = (curve) => { | |
| flow = new Flow(fish); | |
| flow.updateCurve(0, curve); | |
| scene.add(flow.object3D); | |
| }; | |
| const showLineFromPoints = (points, color) => { | |
| if (showLines) { | |
| const line = new THREE.LineLoop( | |
| new THREE.BufferGeometry().setFromPoints(points), | |
| new THREE.LineBasicMaterial({ color: color }) | |
| ); | |
| line.geometry.center(); | |
| scene.add(line); | |
| } | |
| }; | |
| // import | |
| import * as THREE from "https://cdn.skypack.dev/three"; | |
| function transformSVGPath(pathStr) { | |
| const DIGIT_0 = 48, DIGIT_9 = 57, COMMA = 44, SPACE = 32, PERIOD = 46, | |
| MINUS = 45; | |
| const DEGS_TO_RADS = Math.PI/180.0; | |
| var path = new THREE.Shape(); | |
| var idx = 1, len = pathStr.length, activeCmd, | |
| x = 0, y = 0, nx = 0, ny = 0, firstX = null, firstY = null, | |
| x1 = 0, x2 = 0, y1 = 0, y2 = 0, | |
| rx = 0, ry = 0, xar = 0, laf = 0, sf = 0, cx, cy; | |
| function eatNum() { | |
| var sidx, c, isFloat = false, s; | |
| // eat delims | |
| while (idx < len) { | |
| c = pathStr.charCodeAt(idx); | |
| if (c !== COMMA && c !== SPACE) | |
| break; | |
| idx++; | |
| } | |
| if (c === MINUS) | |
| sidx = idx++; | |
| else | |
| sidx = idx; | |
| // eat number | |
| while (idx < len) { | |
| c = pathStr.charCodeAt(idx); | |
| if (DIGIT_0 <= c && c <= DIGIT_9) { | |
| idx++; | |
| continue; | |
| } | |
| else if (c === PERIOD) { | |
| idx++; | |
| isFloat = true; | |
| continue; | |
| } | |
| s = pathStr.substring(sidx, idx); | |
| return isFloat ? parseFloat(s) : parseInt(s); | |
| } | |
| s = pathStr.substring(sidx); | |
| return isFloat ? parseFloat(s) : parseInt(s); | |
| } | |
| function nextIsNum() { | |
| var c; | |
| // do permanently eat any delims... | |
| while (idx < len) { | |
| c = pathStr.charCodeAt(idx); | |
| if (c !== COMMA && c !== SPACE) | |
| break; | |
| idx++; | |
| } | |
| c = pathStr.charCodeAt(idx); | |
| return (c === MINUS || (DIGIT_0 <= c && c <= DIGIT_9)); | |
| } | |
| function eatAbsoluteArc() { | |
| rx = eatNum(); | |
| ry = eatNum(); | |
| xar = eatNum() * DEGS_TO_RADS; | |
| laf = eatNum(); | |
| sf = eatNum(); | |
| nx = eatNum(); | |
| ny = eatNum(); | |
| if( activeCmd == 'a' ) { // relative | |
| nx += x; | |
| ny += y; | |
| } | |
| console.debug( "[SVGPath2ThreeShape.eatAbsoluteArc] Read arc params: rx=" + rx + ", ry=" + ry + ", xar=" + xar + ", laf=" + laf + ", sf=" + sf + ", nx=" + nx + ", ny=" + ny ); | |
| if (rx !== ry) { | |
| console.warn("Forcing elliptical arc to be a circular one :(", | |
| rx, ry); | |
| } | |
| // SVG implementation notes does all the math for us! woo! | |
| // http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes | |
| // step1, using x1 as x1' | |
| x1 = Math.cos(xar) * (x - nx) / 2 + Math.sin(xar) * (y - ny) / 2; | |
| y1 = -Math.sin(xar) * (x - nx) / 2 + Math.cos(xar) * (y - ny) / 2; | |
| // step 2, using x2 as cx' | |
| console.debug( "[SVGPath2ThreeShape.eatAbsoluteArc] TMP x1=" + x1 + ", y1=" + y1 + ", (rx*rx * y1*y1 + ry*ry * x1*x1)=" + (rx*rx * y1*y1 + ry*ry * x1*x1) + ", (rx*rx * ry*ry - rx*rx * y1*y1 - ry*ry * x1*x1)=" + (rx*rx * ry*ry - rx*rx * y1*y1 - ry*ry * x1*x1) ); | |
| var norm = Math.sqrt( Math.abs( | |
| (rx*rx * ry*ry - rx*rx * y1*y1 - ry*ry * x1*x1) / | |
| (rx*rx * y1*y1 + ry*ry * x1*x1) ) ); | |
| if (laf === sf) | |
| norm = -norm; | |
| x2 = norm * rx * y1 / ry; | |
| y2 = norm * -ry * x1 / rx; | |
| console.debug( "[SVGPath2ThreeShape.eatAbsoluteArc] TMP norm=" + norm + ", x2=" + x2 + ", y2=" + y2 ); | |
| // step 3 | |
| cx = Math.cos(xar) * x2 - Math.sin(xar) * y2 + (x + nx) / 2; | |
| cy = Math.sin(xar) * x2 + Math.cos(xar) * y2 + (y + ny) / 2; | |
| console.debug( "[SVGPath2ThreeShape.eatAbsoluteArc] TMP cx=" + cx + ", cy=" + cy ); | |
| var u = new THREE.Vector2(1, 0), | |
| v = new THREE.Vector2((x1 - x2) / rx, | |
| (y1 - y2) / ry); | |
| var startAng = Math.acos(u.dot(v) / u.length() / v.length()); | |
| if (u.x * v.y - u.y * v.x < 0) | |
| startAng = -startAng; | |
| // we can reuse 'v' from start angle as our 'u' for delta angle | |
| u.x = (-x1 - x2) / rx; | |
| u.y = (-y1 - y2) / ry; | |
| var deltaAng = Math.acos(v.dot(u) / v.length() / u.length()); | |
| // This normalization ends up making our curves fail to triangulate... | |
| if (v.x * u.y - v.y * u.x < 0) | |
| deltaAng = -deltaAng; | |
| if (!sf && deltaAng > 0) | |
| deltaAng -= Math.PI * 2; | |
| if (sf && deltaAng < 0) | |
| deltaAng += Math.PI * 2; | |
| console.debug( "[SVGPath2ThreeShape.eatAbsoluteArc] Building arc from values: cx=" + cx + ", cy=" + cy + ", startAng=" + startAng + ", deltaAng=" + deltaAng + ", endAng=" + (startAng+deltaAng) + ", sweepFlag=" + sf ); | |
| path.absarc(cx, cy, rx, startAng, startAng + deltaAng, sf); | |
| x = nx; | |
| y = ny; | |
| } | |
| var canRepeat; | |
| activeCmd = pathStr[0]; | |
| while (idx <= len) { | |
| canRepeat = true; | |
| switch (activeCmd) { | |
| // moveto commands, become lineto's if repeated | |
| case 'M': | |
| x = eatNum(); | |
| y = eatNum(); | |
| path.moveTo(x, y); | |
| activeCmd = 'L'; | |
| break; | |
| case 'm': | |
| x += eatNum(); | |
| y += eatNum(); | |
| path.moveTo(x, y); | |
| activeCmd = 'l'; | |
| break; | |
| case 'Z': | |
| case 'z': | |
| canRepeat = false; | |
| if (x !== firstX || y !== firstY) | |
| path.lineTo(firstX, firstY); | |
| break; | |
| // - lines! | |
| case 'L': | |
| case 'H': | |
| case 'V': | |
| nx = (activeCmd === 'V') ? x : eatNum(); | |
| ny = (activeCmd === 'H') ? y : eatNum(); | |
| path.lineTo(nx, ny); | |
| x = nx; | |
| y = ny; | |
| break; | |
| case 'l': | |
| case 'h': | |
| case 'v': | |
| nx = (activeCmd === 'v') ? x : (x + eatNum()); | |
| ny = (activeCmd === 'h') ? y : (y + eatNum()); | |
| path.lineTo(nx, ny); | |
| x = nx; | |
| y = ny; | |
| break; | |
| // - cubic bezier | |
| case 'C': | |
| x1 = eatNum(); y1 = eatNum(); | |
| case 'S': | |
| if (activeCmd === 'S') { | |
| x1 = 2 * x - x2; y1 = 2 * y - y2; | |
| } | |
| x2 = eatNum(); | |
| y2 = eatNum(); | |
| nx = eatNum(); | |
| ny = eatNum(); | |
| path.bezierCurveTo(x1, y1, x2, y2, nx, ny); | |
| x = nx; y = ny; | |
| break; | |
| case 'c': | |
| x1 = x + eatNum(); | |
| y1 = y + eatNum(); | |
| case 's': | |
| if (activeCmd === 's') { | |
| x1 = 2 * x - x2; | |
| y1 = 2 * y - y2; | |
| } | |
| x2 = x + eatNum(); | |
| y2 = y + eatNum(); | |
| nx = x + eatNum(); | |
| ny = y + eatNum(); | |
| path.bezierCurveTo(x1, y1, x2, y2, nx, ny); | |
| x = nx; y = ny; | |
| break; | |
| // - quadratic bezier | |
| case 'Q': | |
| x1 = eatNum(); y1 = eatNum(); | |
| case 'T': | |
| if (activeCmd === 'T') { | |
| x1 = 2 * x - x1; | |
| y1 = 2 * y - y1; | |
| } | |
| nx = eatNum(); | |
| ny = eatNum(); | |
| path.quadraticCurveTo(x1, y1, nx, ny); | |
| x = nx; | |
| y = ny; | |
| break; | |
| case 'q': | |
| x1 = x + eatNum(); | |
| y1 = y + eatNum(); | |
| case 't': | |
| if (activeCmd === 't') { | |
| x1 = 2 * x - x1; | |
| y1 = 2 * y - y1; | |
| } | |
| nx = x + eatNum(); | |
| ny = y + eatNum(); | |
| path.quadraticCurveTo(x1, y1, nx, ny); | |
| x = nx; y = ny; | |
| break; | |
| // - elliptical arc | |
| case 'A': | |
| // eatAbsoluteArc(); | |
| case 'a': | |
| eatAbsoluteArc(); | |
| break; | |
| default: | |
| throw new Error("weird path command: " + activeCmd); | |
| } | |
| if (firstX === null) { | |
| firstX = x; | |
| firstY = y; | |
| } | |
| // just reissue the command | |
| if (canRepeat && nextIsNum()) | |
| continue; | |
| activeCmd = pathStr[idx++]; | |
| } | |
| return path; | |
| } | |
| export default transformSVGPath |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment