I stucked a little.
I have a code that is working in codepen and if I paste it in opened F12 window it will work fine, so it is ok.
The idea of this svg code was that I can hover not exactly on path, but somewhere near, for it to change color.
But I need to generate exact same code using javascipt, I wrote 2 classes, used them, they now generate same code as in codepen link, I can see it in F12 dev window, but it doesn't work.
My javascript code:
class Polyline {
// https://codepen.io/francoisromain/pen/dzoZZj
constructor(points, smoothing, fill, color) {
this.points = points;
this.smoothing = (smoothing === undefined) ? 0.2 : smoothing;
this.fill = fill;
this.color = color;
}
line (pointA, pointB) {
const lengthX = pointB[0] - pointA[0];
const lengthY = pointB[1] - pointA[1];
return {
length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
angle: Math.atan2(lengthY, lengthX)
}
}
controlPoint (current, previous, next, reverse) {
const p = previous || current;
const n = next || current;
const o = this.line(p, n);
const angle = o.angle + (reverse ? Math.PI : 0);
const length = o.length * this.smoothing;
const x = current[0] + Math.cos(angle) * length;
const y = current[1] + Math.sin(angle) * length;
return [x, y];
}
bezierCommand(point, i, a) {
const cps = this.controlPoint(a[i - 1], a[i - 2], point);
const cpe = this.controlPoint(point, a[i - 1], a[i + 1], true);
return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`;
}
svgPath(points, svg) {
const d = points.reduce((acc, point, i, a) => i === 0
? `M ${point[0]},${point[1]}`
: `${acc} ${this.bezierCommand(point, i, a)}`
, '');
const defs = document.createElement( 'defs' );
defs.innerHTML = "<path id='poly-1' d='" + d + "' fill=" + this.fill + "></path>";
svg.appendChild(defs);
const g = document.createElement( 'g' );
g.setAttribute("id", "group");
svg.appendChild(g);
const use1 = document.createElement( 'use' );
use1.setAttribute("xlink:href", "#poly-1");
use1.setAttribute("stroke-width", "15");
use1.setAttribute("pointer-events", "stroke");
g.appendChild(use1);
const use2 = document.createElement( 'use' );
use2.setAttribute("class", "use");
use2.setAttribute("xlink:href", "#poly-1");
g.appendChild(use2);
}
draw() {
const svg = document.getElementById("chart-main-canvas");
this.svgPath(this.points, svg);
}
}
class Canvas {
constructor(w, h, color) {
this.width = w;
this.height = h;
this.color = color;
// this.scale = 1;
}
canvas() {
return `<svg width="${this.width}" height="${this.height}" id="chart-main-canvas" style="background-color: ${this.color}; z-index:5000;" ></svg>`;
}
draw() {
const c = document.getElementById("lifespan-chart-content");
c.innerHTML = this.canvas();
}
}
window.addEventListener('load', function() {
c1 = new Canvas(1000, 500, "bisque");
c1.draw();
var smoothing = 0.2;
var fill = "green";
var color = "red";
const points = [
[5, 10],
[100, 400],
[200, 400],
[355, 50],
[500, 500]
]
p1 = new Polyline(points, smoothing, fill, color);
p1.draw();
});
Why and how I can fix it?
Another additional link on working svg example:link
PS I pretty sure that the problem method is svgPath in Polyline class
edit 1
edited function:
svgPath(points, svg) {
const d = points.reduce((acc, point, i, a) => i === 0
? `M ${point[0]},${point[1]}`
: `${acc} ${this.bezierCommand(point, i, a)}`
, '');
const defs = document.createElementNS("http://www.w3.org/2000/svg", 'defs' );
defs.innerHTML = "<path id='poly-1' d='" + d + "' fill=" + this.fill + "></path>";
svg.appendChild(defs);
const g = document.createElementNS("http://www.w3.org/2000/svg", 'g' );
g.setAttribute("id", "group");
svg.appendChild(g);
const use1 = document.createElementNS("http://www.w3.org/2000/svg",'use' );
use1.setAttribute("xlink:href", "#poly-1");
use1.setAttribute("stroke-width", "15");
use1.setAttribute("pointer-events", "stroke");
g.appendChild(use1);
const use2 = document.createElementNS("http://www.w3.org/2000/svg", 'use' );
use2.setAttribute("class", "use");
use2.setAttribute("xlink:href", "#poly-1");
g.appendChild(use2);
}
still doesnt work
You can also use this technique to draw SVG, because it is not that simple to just innerHTML them
// Your SVG code (formed as you wish)
const svgFromJS = `<svg width="1000" height="500" id="chart-main-canvas" style="background-color: bisque; z-index:5000;">
<defs>
<path id="poly-1" d="M 5,10 C 24,88 60.99999999999998,322 100,400 C 139,478 149,470 200,400 C 251,330 295,30.000000000000007 355,50 C 415,70 470.99999999999994,410 500,500" fill="none"></path>
</defs>
<g id="group">
<use xlink:href="#poly-1" stroke-width="15" pointer-events="stroke"></use>
<use class="use" xlink:href="#poly-1"></use>
</g>
</svg>`;
// Create a tmp container
const c = document.createElement('div');
// Add SVG to tmp container
c.innerHTML = '' + svgFromJS;
// Move childs of the SVG inside tmp container to target SVG at the body
Array.prototype.slice.call(c.childNodes[0].childNodes).forEach(function(el) {
document.getElementById('swgtodraw').appendChild(el);
});
#group .use{
stroke:red
}
#group:hover .use{
stroke: green;
}
<svg id="swgtodraw" width="1000" height="500" style="background-color: bisque; z-index:5000;"></svg>
Related
I am trying to convert text to svg in my React web app, but the resulted svg code which i am getting is not right, t's not showing svg.
Code:
import opentype from "opentype.js"
async function textToPath(){
const text = "Hello"
const size = {x: 50, y: 25}
const fontFile = "https://fonts.gstatic.com/s/firasans/v15/va9E4kDNxMZdWfMOD5Vvl4jO.ttf"
let params = {
string: text,
font: fontFile,
fontSize: size.x,
decimals: 1,
singleGylyphs: false
}
const font = await opentype.load(params.font);
let options = params.options;
let unitsPerEm = font.unitsPerEm;
let ratio = params.fontSize / unitsPerEm;
let ascender = font.ascender;
let descender = Math.abs(font.descender);
let ratAsc = ascender / unitsPerEm;
let ratDesc = descender / unitsPerEm;
let yOffset = params.fontSize * ratAsc;
let lineHeight = params.fontSize + params.fontSize * ratDesc;
let singleGylyphs = params.singleGylyphs;
let teststring = params.string.split("");
let glyphs = font.stringToGlyphs(params.string);
let leftSB = glyphs[0].leftSideBearing * ratio;
let textPath = "";
//individual paths for each glyph
if (singleGylyphs) {
let paths = font.getPaths(
params.string,
-leftSB,
yOffset,
params.fontSize,
options
);
paths.forEach(function (path, i) {
let pathEl = path.toSVG(params.decimals);
textPath += pathEl.replaceAll(
"d=",
'class="glyph glyph-' + teststring[i] + '" d='
);
});
}
//word (all glyphs) merged to one path
else {
let path = font.getPath(
params.string,
-leftSB,
yOffset,
params.fontSize,
options
);
textPath += path
.toSVG(params.decimals)
.replaceAll("d=", 'class="glyph" d=');
}
// render
let fontSvgWrp = document.createElement("div");
fontSvgWrp.classList.add("fontSvgWrp");
let fontSvg = document.createElementNS(
"http://www.w3.org/2000/svg",
"svg"
);
fontSvg.classList.add("svgText");
fontSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
fontSvg.innerHTML = textPath;
fontSvgWrp.appendChild(fontSvg);
// adjust bbox
let bb = fontSvg.getBBox();
let stringWidth = Math.ceil(bb.width + bb.x);
fontSvg.setAttribute("viewBox", "0 0 " + stringWidth + " " + lineHeight);
fontSvg.setAttribute("width", size.x);
fontSvg.setAttribute("data-asc", ratAsc);
console.log(fontSvg); // getting svg code
let textPathSvg = fontSvg.querySelector(".glyph");
let textPathLength = textPathSvg.getTotalLength();
return textPathLength
}
It's generating this svg which is not working:
<svg class="svgText" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 5013.25" width="50" data-asc="0.935"><path class="glyph" d="M24 46.8L19.3 46.8L19.3 30.6L4.8 30.6L4.8 46.8L0 46.8L0 12.3L4.8 12.3L4.8 26.7L19.3 26.7L19.3 12.3L24 12.3L24 46.8ZM53.7 32.8Q53.7 34.0 53.5 35.1L53.5 35.1L36.8 35.1Q37.0 39.5 39.0 41.5Q40.9 43.6 43.9 43.6L43.9 43.6Q45.8 43.6 47.4 43.0Q49.0 42.5 50.7 41.3L50.7 41.3L52.7 44.0Q48.5 47.4 43.5 47.4L43.5 47.4Q38 47.4 34.9 43.8Q31.9 40.1 31.9 33.9L31.9 33.9Q31.9 29.8 33.2 26.6Q34.5 23.4 37.0 21.6Q39.5 19.8 42.8 19.8L42.8 19.8Q48.0 19.8 50.9 23.3Q53.7 26.7 53.7 32.8L53.7 32.8ZM49.1 31.8L49.1 31.4Q49.1 27.5 47.5 25.5Q46 23.4 42.9 23.4L42.9 23.4Q37.3 23.4 36.8 31.8L36.8 31.8L49.1 31.8ZM66.3 47.4Q63.7 47.4 62.2 45.8Q60.8 44.3 60.8 41.5L60.8 41.5L60.8 9.8L65.3 9.3L65.3 41.5Q65.3 42.5 65.7 43.0Q66.1 43.5 67 43.5L67 43.5Q68.0 43.5 68.7 43.3L68.7 43.3L69.9 46.5Q68.3 47.4 66.3 47.4L66.3 47.4ZM80.9 47.4Q78.4 47.4 76.9 45.8Q75.4 44.3 75.4 41.5L75.4 41.5L75.4 9.8L80 9.3L80 41.5Q80 42.5 80.4 43.0Q80.8 43.5 81.7 43.5L81.7 43.5Q82.6 43.5 83.4 43.3L83.4 43.3L84.6 46.5Q82.9 47.4 80.9 47.4L80.9 47.4ZM100.2 19.8Q105.8 19.8 108.8 23.5Q111.9 27.2 111.9 33.5L111.9 33.5Q111.9 37.6 110.5 40.8Q109.1 43.9 106.5 45.6Q103.8 47.4 100.2 47.4L100.2 47.4Q94.6 47.4 91.5 43.6Q88.4 40.0 88.4 33.6L88.4 33.6Q88.4 29.5 89.8 26.4Q91.2 23.3 93.9 21.5Q96.5 19.8 100.2 19.8L100.2 19.8ZM100.2 23.5Q93.4 23.5 93.4 33.6L93.4 33.6Q93.4 43.6 100.2 43.6L100.2 43.6Q107.0 43.6 107.0 33.5L107.0 33.5Q107.0 23.5 100.2 23.5L100.2 23.5Z"></path></svg>
I tried & searched a lot but i didn't get anything to fix this thing and why the converted svg is not working. Can someone help?
The svg output is actually OK but your viewBox isn't calculated correctly.
The viewBox width value is 0 – so your path is completely cropped/invisible.
The main problem in your script: you need to append the svg to your DOM otherwise getBBox() will return an empty object (or width:0, height:0).
It should rather look like this:
fontSvgWrp.appendChild(fontSvg);
document.body.appendChild(fontSvgWrp);
// adjust bbox
let bb = fontSvg.getBBox();
Example: appending svg before getBBox()
let fontSVG = textToPath();
async function textToPath(){
const text = "Hello"
const size = {x: 50, y: 25}
const fontFile = "https://fonts.gstatic.com/s/firasans/v15/va9E4kDNxMZdWfMOD5Vvl4jO.ttf"
let params = {
string: text,
font: fontFile,
fontSize: size.x,
decimals: 1,
singleGylyphs: false
}
const font = await opentype.load(params.font);
let options = params.options;
let unitsPerEm = font.unitsPerEm;
let ratio = params.fontSize / unitsPerEm;
let ascender = font.ascender;
let descender = Math.abs(font.descender);
let ratAsc = ascender / unitsPerEm;
let ratDesc = descender / unitsPerEm;
let yOffset = params.fontSize * ratAsc;
let lineHeight = params.fontSize + params.fontSize * ratDesc;
let singleGylyphs = params.singleGylyphs;
let teststring = params.string.split("");
let glyphs = font.stringToGlyphs(params.string);
let leftSB = glyphs[0].leftSideBearing * ratio;
let textPath = "";
//individual paths for each glyph
if (singleGylyphs) {
let paths = font.getPaths(
params.string,
-leftSB,
yOffset,
params.fontSize,
options
);
paths.forEach(function (path, i) {
let pathEl = path.toSVG(params.decimals);
textPath += pathEl.replaceAll(
"d=",
'class="glyph glyph-' + teststring[i] + '" d='
);
});
}
//word (all glyphs) merged to one path
else {
let path = font.getPath(
params.string,
-leftSB,
yOffset,
params.fontSize,
options
);
textPath += path
.toSVG(params.decimals)
.replaceAll("d=", 'class="glyph" d=');
}
// render
let fontSvgWrp = document.createElement("div");
fontSvgWrp.classList.add("fontSvgWrp");
let fontSvg = document.createElementNS(
"http://www.w3.org/2000/svg",
"svg"
);
fontSvg.classList.add("svgText");
fontSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
fontSvg.innerHTML = textPath;
fontSvgWrp.appendChild(fontSvg);
document.body.appendChild(fontSvgWrp);
// adjust bbox
let bb = fontSvg.getBBox();
//console.log(bb)
let stringWidth = Math.ceil(bb.width + bb.x);
//let stringWidth = Math.ceil(bb.width);
fontSvg.setAttribute("viewBox", "0 0 " + stringWidth + " " + lineHeight);
//fontSvg.setAttribute("width", size.x);
fontSvg.setAttribute("data-asc", ratAsc);
console.log(fontSvg); // getting svg code
let textPathSvg = fontSvg.querySelector(".glyph");
let textPathLength = textPathSvg.getTotalLength();
return textPathLength
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/opentype.js/1.3.4/opentype.min.js"></script>
Alternative: calculate total width from glyph data
If you don't want to append the svg you could also calculate the viewBox width using the getAdvanceWidth() (for seperate glyph paths) or getBoundingBox() method:
let firstGlyph = glyphs[0];
let lastGlyph = glyphs[glyphs.length-1];
let firstSideBearing = firstGlyph.leftSideBearing * ratio;
let lastSideBearing = (lastGlyph.advanceWidth - lastGlyph.xMax) * ratio;
let stringWidth = 0;
if (!singleGylyphs) {
stringWidth = Math.ceil(path.getBoundingBox().x2);
} else{
stringWidth = Math.ceil(font.getAdvanceWidth(params.string, params.fontSize, options) - leftSB - lastRightSideBearing);
}
let fontSVG = textToPath();
async function textToPath() {
const text = "Hej";
const size = { x: 50, y: 25 };
const fontFile =
"https://fonts.gstatic.com/s/firasans/v15/va9E4kDNxMZdWfMOD5Vvl4jO.ttf";
let params = {
string: text,
font: fontFile,
fontSize: size.x,
decimals: 1,
singleGylyphs: true
};
const font = await opentype.load(params.font);
let options = params.options;
let unitsPerEm = font.unitsPerEm;
let ratio = params.fontSize / unitsPerEm;
let ascender = font.ascender;
let descender = Math.abs(font.descender);
let ratAsc = ascender / unitsPerEm;
let ratDesc = descender / unitsPerEm;
let yOffset = params.fontSize * ratAsc;
let lineHeight = params.fontSize + params.fontSize * ratDesc;
let singleGylyphs = params.singleGylyphs;
let teststring = params.string.split("");
let glyphs = font.stringToGlyphs(params.string);
let firstGlyph = glyphs[0];
let lastGlyph = glyphs[glyphs.length-1];
let leftSB = firstGlyph.leftSideBearing * ratio;
let textPath = "";
let path = "";
let paths = "";
//individual paths for each glyph
if (singleGylyphs) {
paths = font.getPaths(
params.string,
-leftSB,
yOffset,
params.fontSize,
options
);
paths.forEach(function (path, i) {
let pathEl = path.toSVG(params.decimals);
textPath += pathEl.replaceAll(
"d=",
'class="glyph glyph-' + teststring[i] + '" d='
);
});
}
//word (all glyphs) merged to one path
else {
path = font.getPath(
params.string,
-leftSB,
yOffset,
params.fontSize,
options
);
textPath += path
.toSVG(params.decimals)
.replaceAll("d=", 'class="glyph" d=');
}
// render
let fontSvgWrp = document.createElement("div");
fontSvgWrp.classList.add("fontSvgWrp");
let fontSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
fontSvg.classList.add("svgText");
fontSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
fontSvg.innerHTML = textPath;
fontSvgWrp.appendChild(fontSvg);
//calculate viewBox width by getAdvanceWidth() or getBoundingBox()
let lastRightSideBearing = (lastGlyph.advanceWidth - lastGlyph.xMax) * ratio;
let stringWidth = 0;
if (!singleGylyphs) {
stringWidth = Math.ceil(path.getBoundingBox().x2);
} else{
stringWidth = Math.ceil(font.getAdvanceWidth(params.string, params.fontSize, options) - leftSB - lastRightSideBearing);
}
// adjust bbox
fontSvg.setAttribute("viewBox", "0 0 " + stringWidth + " " + lineHeight);
fontSvg.setAttribute("data-asc", ratAsc);
let textPathSvg = fontSvg.querySelector(".glyph");
let textPathLength = textPathSvg.getTotalLength();
document.body.appendChild(fontSvgWrp);
return textPathLength;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/opentype.js/1.3.4/opentype.min.js"></script>
I have the next problem I'm trying to create a draggable world globe with d3 in a svelte app, the problem that i have is when a try to drag the globe the drag action is laggy I think that my problem have someting to do with a memory leak in the draggable component I don't know if this is the main reason of this laggy behaviour. The globe was drew with svg's
This is the App link:
https://codesandbox.io/s/cocky-benz-2mk5bs
Draggable component
<script>
import { onMount, onDestroy, afterUpdate } from 'svelte';
import { mouseAdjusted } from '../lib/Utilities';
onMount(() => console.log('mounted'));
afterUpdate(() => {
//console.log('Destroyed');
});
export let mouseX;
export let mouseY;
let offset;
let isMove = false;
const start = (e) => {
isMove = true;
const element = e.target;
const target = e.target.parentElement.parentElement.getScreenCTM();
// get the mouse position in the svg shape
offset = mouseAdjusted(e.clientX, e.clientY, target);
// rest the shape mouse posicion from the shape x y coordinates
offset[1] -= +element.getAttributeNS(null, 'y');
offset[0] -= +element.getAttributeNS(null, 'x');
};
const move = (e) => {
if (isMove) {
const target = e.target.parentElement.parentElement.getScreenCTM();
const [x, y] = mouseAdjusted(e.clientX, e.clientY, target);
mouseX = offset[0] - x;
mouseY = offset[1] - y;
}
};
const end = () => {
isMove = false;
offset = null;
};
</script>
<svelte:window
on:mousemove|preventDefault|stopPropagation={move}
on:mouseup|preventDefault|stopPropagation={end}
/>
<g class="draggable" on:mousedown|preventDefault|stopPropagation={start}>
<slot />
</g>
Graph component
<script>
import { onMount, onDestroy } from "svelte";
import * as d3 from "d3";
import * as topojson from "topojson-client";
import Draggable from "./draggable.svelte";
let width = 500;
$: width = width * 0.95;
let height = 500;
$: height = height * 0.98;
let world;
let x;
let y;
let projection;
let path;
let ROTATION = [-45, -65, 0];
// Add topojsom
onMount(async () => {
const resp = await d3.json(
"/src/assets/world-administrative-boundaries.json"
);
world = await topojson.feature(resp, "world-administrative-boundaries");
});
$: ROTATION = [-x, y, 0];
$: projection = d3
.geoOrthographic()
.fitSize([width, height], world)
.scale([width / 3])
.rotate(ROTATION);
$: path = d3.geoPath(projection);
const fillScale = d3
.scaleOrdinal()
.domain(["", ""])
.range([" hsl(40, 98%, 47%)", "hsl(9, 64%, 50%)"])
.unknown("hsl(227, 16%, 42%)");
const strokeScale = d3
.scaleOrdinal()
.domain(["", ""])
.range([" hsl(40, 98%, 35%)", "hsl(9, 64%, 35%)"])
.unknown("hsl(227, 16%, 22%)");
// transform to features
$: countries = world?.features.map(obj => {
const { geometry, properties, _ } = obj;
//const d = path(geometry);
const fill = fillScale(properties.iso3);
const stroke = strokeScale(properties.iso3);
const newProperties = Object.assign({ ...properties }, { fill, stroke });
return Object.assign({}, { geometry, properties: newProperties });
});
// $: console.log('features', features);
//$: console.log('first', ROTATION);
</script>
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />
<svg {width} {height}>
<Draggable bind:mouseX={x} bind:mouseY={y}>
{#if countries}
<path class="sphere" d={path({ type: 'Sphere' })} />
<g class="countries">
{#each countries as country}
<path
d={path(country.geometry)}
fill={country.properties.fill}
stroke={country.properties.stroke}
/>
{/each}
</g>
{/if}
</Draggable>
</svg>
<!-- markup (zero or more items) goes here -->
<style>
svg {
border: 1px solid tomato;
}
.sphere {
fill: var(--base-color-2);
}
</style>
I'm writing a component (React) which draws a fancy box around a bit of text with SVG. The component receives the width and height of the dom element and draws a polygon derived from those values. Simplified example follows:
import React from 'react';
const Box = ({ dimensions }) => {
const { width, height } = dimensions;
const mod = 10;
const tlX = 0;
const tlY = 0;
const trX = tlX + width;
const trY = tlY;
const brX = trX;
const brY = trY + height;
const blX = 0;
const blY = tlY + height;
return (
<picture>
<svg height={height + 50} width={width + 200}>
<polygon
points={`${tlX},${tlY} ${trX},${trY} ${brX},${brY} ${blX},${blY}`}
style={{ fill: 'black', fillOpacity: '0.5' }}
/>
</svg>
</picture>
);
};
In this stripped down example the result is a rectangle with straight corners based on the width and height of the supplied dom element. In reality these values are given some random modifiers to create more of a trapezoid, as in fig A.
Illustration of desired result
In the fig B you can see my problem with this method. When drawing a longer or shorter box, the figure looks squished. What I want is for the box to behave like in fig C, in that it will draw the horizontal lines at a given angle until it has reached a certain width.
From what I can intuit this should be possible with some math savvy, but I am unable to quite figuring it out on my own.
Thanks in advance for any input, and please let me know if I'm being unclear on anything.
Edit:
A "trapezoid" shape is apparently not what I'm looking for. My apologies. I just want a sort of janky rectangle. I was asked to show the code I've been using in more detail. As you will see I am basically just taking the values from the last example and messing them up a bit by adding or subtracting semi-randomly.
import React from 'react';
import PropTypes from 'prop-types';
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
const point = (core, edge) => getRandomArbitrary(core - edge, core + edge);
const Box = ({ dimensions }) => {
const { width, height } = dimensions;
const mod = 10;
const tlX = point(25, mod);
const tlY = point(40, mod);
const trX = point(width + 55, mod);
const trY = point(25, mod);
const brX = point(width + 25, mod);
const brY = point(height - 25, mod);
const blX = point(5, mod);
const blY = point(height - 40, mod);
return (
<picture>
<svg height={height + 50} width={width + 200}>
<polygon
points={`${tlX},${tlY} ${trX},${trY} ${brX},${brY} ${blX},${blY}`}
style={{ fill: 'black', fillOpacity: '0.5' }}
/>
</svg>
</picture>
);
};
Box.propTypes = {
dimensions: PropTypes.shape({
width: PropTypes.number,
height: PropTypes.number,
}).isRequired,
};
export default Box;
In order to calculate the y for the top right point I'm imagining a circle with the center in the top left point. The radius of the circle is tlX - trX, The angle is -5 degs but you can change it to what you need. In order to calculate the value for the y you can do
const trY = tlY - (tlX - trX)*Math.sin(a1)
To calculate the y for the bottom right point I'm doing the same only this time the angle is 5 degs, the center in the bottom left point and the radius of the circle is blX - brX
const brY = blY - (blX - brX)*Math.sin(a2)
The important part of the demo is this:
//calculating the points for the polygon
const tlX = BB.x-10;
const tlY = BB.y-5;
const trX = tlX + 20 + BB.width;
//const trY = tlY - 10;
const trY = tlY - (tlX - trX)*Math.sin(a1)
const brX = trX - 5;
const blX = tlX + 5;
const blY = tlY + 10 + BB.height;
//const brY = trY + 30+ BB.height;
const brY = blY - (blX - brX)*Math.sin(a2)
Next comes a demo where I'm using plain javascript. Please change the length of the text to see if this is what you need.
let bb = txt.getBBox();
let m = 10;
// the blue rect
updateSVGelmt({x:bb.x-m,y:bb.y-m,width:bb.width+2*m,height:bb.height+2*m},theRect)
// the bounding box of the blue rect
let BB = theRect.getBBox();
//the angles for the polygon
let a1 = -5*Math.PI/180;
let a2 = -a1;
//calculating the points for the polygon
const tlX = BB.x-10;
const tlY = BB.y-5;
const trX = tlX + 20 + BB.width;
//const trY = tlY - 10;
const trY = tlY - (tlX - trX)*Math.sin(a1)
const brX = trX - 5;
const blX = tlX + 5;
const blY = tlY + 10 + BB.height;
//const brY = trY + 30+ BB.height;
const brY = blY - (blX - brX)*Math.sin(a2)
let points = `${tlX},${tlY} ${trX},${trY} ${brX},${brY} ${blX},${blY}`;
poly.setAttributeNS(null, "points", points)
let polybox = poly.getBBox();
svg.setAttributeNS(null, "viewBox", `${polybox.x-2} ${polybox.y-2} ${polybox.width+4} ${polybox.height+4}`)
svg.setAttributeNS(null, "width",3*(polybox.width+4))
function updateSVGelmt(o,elmt) {
for (let name in o) {
if (o.hasOwnProperty(name)) {
elmt.setAttributeNS(null, name, o[name]);
}
}
}
svg{border:1px solid}
<svg id="svg">
<polygon id="poly"
points=""
style="stroke: black; fill:none"
/>
<rect id="theRect" fill="#d9d9ff" />
<text id="txt" text-anchor="middle">element</text>
</svg>
I have an issue: I have to change element position&angle on scroll event. Like my webpage is a long road with background - and I need to animate car movements along this path during the scroll.
I good explanation is here: http://prinzhorn.github.io/skrollr-path/ - a perfect solution, that meets my requirements. But Unfortunately it's extremely outdated.
Maybe somebody has an up to date solution-library? Or code-ideas, how to animate element via svg-path with page scrolling?
Also i tried http://scrollmagic.io/examples/expert/bezier_path_animation.html - but it's not something that I need, because my path is difficult. Not just a couple of circles.
Here is some vanilla Javascript that moves a "car" along a path according to how much the page has scrolled.
It should work in all (most) browsers. The part that you may need to tweak is how we get the page height (document.documentElement.scrollHeight). You may need to use different methods depending on the browsers you want to support.
function positionCar()
{
var scrollY = window.scrollY || window.pageYOffset;
var maxScrollY = document.documentElement.scrollHeight - window.innerHeight;
var path = document.getElementById("path1");
// Calculate distance along the path the car should be for the current scroll amount
var pathLen = path.getTotalLength();
var dist = pathLen * scrollY / maxScrollY;
var pos = path.getPointAtLength(dist);
// Calculate position a little ahead of the car (or behind if we are at the end), so we can calculate car angle
if (dist + 1 <= pathLen) {
var posAhead = path.getPointAtLength(dist + 1);
var angle = Math.atan2(posAhead.y - pos.y, posAhead.x - pos.x);
} else {
var posBehind = path.getPointAtLength(dist - 1);
var angle = Math.atan2(pos.y - posBehind.y, pos.x - posBehind.x);
}
// Position the car at "pos" totated by "angle"
var car = document.getElementById("car");
car.setAttribute("transform", "translate(" + pos.x + "," + pos.y + ") rotate(" + rad2deg(angle) + ")");
}
function rad2deg(rad) {
return 180 * rad / Math.PI;
}
// Reposition car whenever there is a scroll event
window.addEventListener("scroll", positionCar);
// Position the car initially
positionCar();
body {
min-height: 3000px;
}
svg {
position: fixed;
}
<svg width="500" height="500"
viewBox="0 0 672.474 933.78125">
<g transform="translate(-54.340447,-64.21875)" id="layer1">
<path d="m 60.609153,64.432994 c 0,0 -34.345187,72.730986 64.649767,101.015256 98.99494,28.28427 321.2285,-62.62946 321.2285,-62.62946 0,0 131.31984,-52.527932 181.82746,16.16244 50.50763,68.69037 82.04198,196.41856 44.44671,284.86302 -30.25843,71.18422 -74.75128,129.29952 -189.90867,133.34013 -115.15739,4.04061 -72.73099,-153.54318 -72.73099,-153.54318 0,0 42.42641,-129.29953 135.36044,-119.198 92.93404,10.10152 -14.14213,-129.29953 -141.42135,-94.95434 -127.27922,34.34518 -183.84777,80.8122 -206.07112,121.2183 -22.22336,40.40611 -42.06243,226.23742 -26.26397,305.06607 8.77013,43.75982 58.20627,196.1403 171.72594,270.72088 73.8225,48.50019 181.82745,2.02031 181.82745,2.02031 0,0 94.95434,-12.12183 78.7919,-155.56349 -16.16244,-143.44166 -111.68403,-138.77778 -139.9683,-138.77778 -28.28427,0 83.39976,-156.18677 83.39976,-156.18677 0,0 127.27922,-189.90867 107.07617,16.16245 C 634.3758,640.21994 864.69058,888.71747 591.94939,941.2454 319.2082,993.77334 -16.162441,539.20469 153.54319,997.81395"
id="path1"
style="fill:none;stroke:#ff0000;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<path id="car" d="M-15,-10 L15,0 L -15,10 z" fill="yellow" stroke="red" stroke-width="7.06"/>
</g>
</svg>
I adapted Paul LeBau's answer for my purposes in TypeScript and figured I'd leave it here in case it's helpful for anyone.
NOTE: It's broken as hell when running the snippet in Stack Overflow. No clue why when it functions perfectly in VSCode. First thing I'd recommended is full screening the snippet if you can't see the SVG. Then actually try copy/pasting this into your own project before making any claims about there being an issue with the posted code.
Notable additions from Paul's:
A clickToScroll function allowing you to click anywhere on the svg path and have it scroll accordingly (does not work at all in the SO snippet window)
The car (rider) will face towards the direction it's moving
Here's the Typescript version separately as SO doesn't support TS :(
interface PathRider {
ride: () => void;
clickToScroll: (e: MouseEvent) => void;
onClick: (e: MouseEvent, callback: (pt: DOMPoint) => void) => void;
};
const usePathRider = (
rider: SVGPathElement,
path: SVGPathElement,
rideOnInit = true
): PathRider => {
const maxScrollY = document.documentElement.scrollHeight - window.innerHeight;
const pathLen = path.getTotalLength();
//====================
/* Helper Functions */
const radToDeg = (rad: number) => (180 * rad) / Math.PI;
const distance = (a: DOMPoint, b: DOMPoint) =>
Math.sqrt(Math.pow(a.y - b.y, 2) + Math.pow(a.x - b.x, 2));
//=========================
/* Click-based Functions */
const step = 0.5; // how granularly it should check for the point on the path closest to where the user clicks. the lower the value the less performant the operation is
let currLen = -step;
const pointArr: DOMPoint[] = [];
while ((currLen += step) <= pathLen)
pointArr.push(path.getPointAtLength(currLen));
const onClick = (e: MouseEvent, callback: (pt: DOMPoint) => void) => {
let pt = new DOMPoint(e.clientX, e.clientY);
callback(pt.matrixTransform(path.getScreenCTM().inverse()));
};
const getLengthAtPoint = (pt: DOMPoint) => {
let bestGuessIdx = 0;
let bestGuessDist = Number.MAX_VALUE;
let guessDist: number;
pointArr.forEach((point, idx) => {
if ((guessDist = distance(pt, point)) < bestGuessDist) {
bestGuessDist = guessDist;
bestGuessIdx = idx;
}
});
return bestGuessIdx * step;
};
const getScrollPosFromLength = (len: number) => (len * maxScrollY) / pathLen;
const clickToScroll = (e: MouseEvent) => {
onClick(e, (point) => {
const lengthAtPoint = getLengthAtPoint(point);
const scrollPos = getScrollPosFromLength(lengthAtPoint);
window.scrollTo({
top: scrollPos,
behavior: 'smooth',
});
});
};
//==========================
/* Scroll-based functions */
let lastDist: number; // for determining direction
const ride = () => {
const scrollY = window.scrollY || window.pageYOffset;
const dist = (pathLen * scrollY) / maxScrollY;
const pos = path.getPointAtLength(dist);
let angle: number;
// calculate position a little ahead of the rider (or behind if we are at the end),
// so we can calculate the rider angle
const dir = lastDist < dist; // true=right
if (dir ? dist + 1 <= pathLen : dist - 1 >= 0) {
const nextPos = path.getPointAtLength(dist + (dir ? 1 : -1));
angle = Math.atan2(nextPos.y - pos.y, nextPos.x - pos.x);
} else {
const nextPos = path.getPointAtLength(dist + (dir ? -1 : 1));
angle = Math.atan2(pos.y - nextPos.y, pos.x - nextPos.x);
}
lastDist = dist;
rider.setAttribute(
'transform',
`translate(${pos.x}, ${pos.y}) rotate(${radToDeg(angle)})`
);
};
if (rideOnInit) ride();
return {
ride,
clickToScroll,
onClick,
};
};
Snippets for running in SO
const usePathRider = (
rider,
path,
rideOnInit = true
) => {
const maxScrollY = document.documentElement.scrollHeight - window.innerHeight;
const pathLen = path.getTotalLength();
const step = 0.5;
let currLen = -step;
const pointArr = [];
while ((currLen += step) <= pathLen)
pointArr.push(path.getPointAtLength(currLen));
//====================
/* Helper Functions */
const radToDeg = (rad) => (180 * rad) / Math.PI;
const distance = (a, b) =>
Math.sqrt(Math.pow(a.y - b.y, 2) + Math.pow(a.x - b.x, 2));
//============
/* Closures */
const onClick = (e, callback) => {
let pt = new DOMPoint(e.clientX, e.clientY);
callback(pt.matrixTransform(path.getScreenCTM().inverse()));
};
const getLengthAtPoint = (pt) => {
let bestGuessIdx = 0;
let bestGuessDist = Number.MAX_VALUE;
let guessDist;
pointArr.forEach((point, idx) => {
if ((guessDist = distance(pt, point)) < bestGuessDist) {
bestGuessDist = guessDist;
bestGuessIdx = idx;
}
});
return bestGuessIdx * step;
};
const getScrollPosFromLength = (len) => (len * maxScrollY) / pathLen;
const clickToScroll = (e) => {
onClick(e, (point) => {
const lengthAtPoint = getLengthAtPoint(point);
const scrollPos = getScrollPosFromLength(lengthAtPoint);
window.scrollTo({
top: scrollPos,
behavior: 'smooth',
});
});
};
let lastDist;
const ride = () => {
const scrollY = window.scrollY || window.pageYOffset;
const dist = (pathLen * scrollY) / maxScrollY;
const pos = path.getPointAtLength(dist);
let angle;
// calculate position a little ahead of the rider (or behind if we are at the end),
// so we can calculate the rider angle
const dir = lastDist < dist; // true=right
if (dir ? dist + 1 <= pathLen : dist - 1 >= 0) {
const nextPos = path.getPointAtLength(dist + (dir ? 1 : -1));
angle = Math.atan2(nextPos.y - pos.y, nextPos.x - pos.x);
} else {
const nextPos = path.getPointAtLength(dist + (dir ? -1 : 1));
angle = Math.atan2(pos.y - nextPos.y, pos.x - nextPos.x);
}
lastDist = dist;
rider.setAttribute(
'transform',
`translate(${pos.x}, ${pos.y}) rotate(${radToDeg(angle)})`
);
};
if (rideOnInit) ride();
return {
ride,
clickToScroll,
onClick,
};
};
/* VANILLA JS USAGE */
const svgRider = document.getElementById('rider');
const svgPath = document.getElementById('path');
const pathRider = usePathRider(svgRider, svgPath);
// Reposition car whenever there is a scroll event
window.addEventListener("scroll", pathRider.ride);
body {
min-height: 3000px;
}
svg {
position: fixed;
}
<svg onclick="pathRider.clickToScroll" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 300 285" shape-rendering="geometricPrecision" text-rendering="geometricPrecision"><path
id="path"
d="M22.061031,163.581791c.750375-2.251126,2.251125-16.508254,26.263131-20.26013s20.26013-8.254128,42.02101,2.251125q21.76088,10.505253-12.006004,18.009005-33.016508.750374,0-18.009005t52.526263,4.427213q6.003001,19.584792,19.134567,14.332166t16.133067-14.332166q32.266133-12.081041,45.772886,0t47.273637,0"
transform="translate(.000002 0.000001)"
fill="none"
stroke="#3f5787"
stroke-width="0.6"
stroke-dasharray="3"
/>
<path
id="rider"
d="M-2,-2 L3,0 L -2,2 z"
stroke="red"
stroke-width="0.6"
/></svg
>
I've the below code that is functioning properly, create the path, and rotate it upon the click.
I want to rotate the path about specific point, when i use the rotate(45 50 50) below instead of the rotate(x) i get this error: VM267:85 Uncaught SyntaxError: missing ) after argument list what shall I do?
Note NOt interested to use any ready library to handle the task, need to so it using the standard API only. thanks
var NS="http://www.w3.org/2000/svg";
var SVG=function(el){
return document.createElementNS(NS,el);
}
svg = SVG('svg');
svg.setAttribute("width", "100%");
svg.setAttribute("height", "100%");
// svg.width='50em'; // Not working
document.body.appendChild(svg);
var bbox = svg.getBoundingClientRect();
var center = {
x: bbox.left + bbox.width/2,
y: bbox.top + bbox.height/2
};
class myPath {
constructor(cx,cy) {
this.path=SVG('path');
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
// https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
var d="M" + cx + " " + cy;
d = d + "L175 120 L125 120 Z";
this.path.setAttribute("d", d);
this.path.setAttribute("fill", "#F7931E");
this.path.addEventListener("click",this,false);
}
get draw(){
return this.path;
}
}
myPath.prototype.rotate = function(x) {
/*
var path = this.path.getBoundingClientRect();
var Pc = {
x: bbox.left + bbox.width/2,
y: bbox.top + bbox.height/2
}; */
return svg.createSVGTransformFromMatrix(svg.createSVGMatrix().rotate(x));
// https://developer.mozilla.org/en/docs/Web/SVG/Attribute/transform
}
myPath.prototype.animate = function() {
self = this.path;
self.transform.baseVal.appendItem(this.rotate(5));
};
myPath.prototype.handleEvent= function(evt){
self = evt.target;
console.log(self.getAttribute('d'));
self.move = setInterval(()=>this.animate(),100);
}
svg.appendChild(new myPath(center.x,center.y).draw);
rotate(45 50 50) is the format for the transform XML attribute. For example:
<path d="..." transform="rotate(45 50 50)" .../>
But you are using the Javascript rotate() function on the SVGTransform object. JS functions require commas between parameters. Try:
rotate(45, 50, 50)
https://developer.mozilla.org/en/docs/Web/API/SVGTransform
I was able to solve it using translate(<x>, <y>) rotate(<a>) translate(-<x>, -<y>) as per this link
var NS="http://www.w3.org/2000/svg";
var SVG=function(el){
return document.createElementNS(NS,el);
}
svg = SVG('svg');
svg.setAttribute("width", "100%");
svg.setAttribute("height", "100%");
svg.setAttribute("fill", "green");
document.body.appendChild(svg);
bbox = svg.getBoundingClientRect();
center = {
x: this.bbox.left + this.bbox.width/2,
y: this.bbox.top + this.bbox.height/2
};
class myPath {
constructor(cx,cy) {
this.path=SVG('path');
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
// https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
var d="M" + cx + " " + cy;
d = d + "h75 v75 h-75 z";
this.path.setAttribute("d", d);
this.path.setAttribute("fill", "#F7931E");
this.path.addEventListener("click",this,false);
this.Pbox = svg.getBoundingClientRect();
this.Pc = {
x: this.Pbox.left + this.Pbox.width/2,
y: this.Pbox.top + this.Pbox.height/2
};
}
get draw(){
return this.path;
}
}
myPath.prototype.rotate = function(x) {
return svg.createSVGTransformFromMatrix(svg.createSVGMatrix().rotate(x));
// https://developer.mozilla.org/en/docs/Web/SVG/Attribute/transform
}
myPath.prototype.move = function(x,y) {
return svg.createSVGTransformFromMatrix(svg.createSVGMatrix().translate(x,y));
// https://developer.mozilla.org/en/docs/Web/SVG/Attribute/transform
}
myPath.prototype.animate = function() {
self = this.path;
self.transform.baseVal.appendItem(this.move(this.Pc.x,this.Pc.y));
self.transform.baseVal.appendItem(this.rotate(5));
self.transform.baseVal.appendItem(this.move(-this.Pc.x,-this.Pc.y));
};
myPath.prototype.handleEvent= function(evt){
self = evt.target;
console.log(self.getAttribute('d'));
self.move = setInterval(()=>this.animate(),100);
}
svg.appendChild(new myPath(center.x,center.y).draw);