英文:
Generate regular polygon in Javascript with base side always at the bottom
问题
这里有一种方法可以生成具有 X
边的正多边形,但基准(第一条边)始终应该水平位于底部。我找到了一些生成器,例如 https://codepen.io/winkerVSbecks/pen/wrZQQm,但它不会在底部生成基准边,并且还有一些“奇怪”的填充在视图框内,我无法去掉它(多边形不会使用 svg 视图框的全宽度和高度,而是在边缘留有空白)。
英文:
Is here a way how to generate regular polygon with X
sides, but the base (first) side should be always horizontal at the bottom. I've found any of generators, e.g. https://codepen.io/winkerVSbecks/pen/wrZQQm, but it does not generate the base side at the bottom and more, it has some "wierd" padding inside the viewbox I could not get rid of (the polygon does not use the full width and height of svg viewbox, but it has spaces at edges).
答案1
得分: 2
根据代码,多边形生成器似乎在右侧边上生成“平坦”的一侧。因此,在绘制多边形时,只需将所有角度顺时针旋转90度(也就是 0.5 * Math.PI
弧度):
function polygon([cx, cy], sideCount, radius) {
return pts(sideCount, radius)
.map(({ r, theta }) => {
// 注意:将 theta 更新为加上顺时针90度的旋转
return [
cx + r * Math.cos(theta + Math.PI * 0.5),
cy + r * Math.sin(theta + Math.PI * 0.5)
];
})
.join(" ");
}
查看 forked CodePen 这里:https://codepen.io/terrymun/pen/LYJQMda
英文:
Based on the code, the polygon generator seems to be generating the "flat" side on the right edge. So it is just a matter of rotating all angles by 90deg clockwise (aka 0.5 * Math.PI
radians) when drawing the polygon:
function polygon([cx, cy], sideCount, radius) {
return pts(sideCount, radius)
.map(({ r, theta }) => {
// NOTE: Update theta to add 90deg clockwise rotation
return [
cx + r * Math.cos(theta + Math.PI * 0.5),
cy + r * Math.sin(theta + Math.PI * 0.5)
];
})
.join(" ");
}
See forked CodePen here: https://codepen.io/terrymun/pen/LYJQMda
答案2
得分: 2
你可以将所有的代码转换为本机JavaScript Web组件(JSWC):
<svg-polygon sides="8" fill="red"></svg-polygon>
<svg-polygon sides="9" fill="yellow"></svg-polygon>
<svg-polygon sides="10" fill="blue"></svg-polygon>
customElements.define("svg-polygon", class extends HTMLElement {
connectedCallback() {
var radians = (deg) => (Math.PI * deg) / 180;
var sides = +this.getAttribute("sides") || 8;
var radius = +this.getAttribute("radius") || 200;
var vb = 2*radius + (this.getAttribute("padding") || 50);
var points = Array(sides).fill(radians(90-((180-(360/sides))/2)))
.map((offset,idx) => [
vb/2 + radius*Math.cos(offset+radians((360/sides)*idx) + Math.PI/2),
vb/2 + radius*Math.sin(offset+radians((360/sides)*idx) + Math.PI/2)
]);
this.innerHTML = `<svg viewBox="0 0 ${vb} ${vb}">` +
`<polygon points="${points.join(" ")}" fill="${this.getAttribute("fill")}"/>` +
`</svg>`;
}
})
svg { width:180px; background:pink }
<svg-polygon sides="8" fill="red"></svg-polygon>
<svg-polygon sides="9" fill="yellow"></svg-polygon>
<svg-polygon sides="10" fill="blue"></svg-polygon>
英文:
You can reduce all that code to a native JavaScript Web Component (JSWC):
<svg-polygon sides="8" fill="red"></svg-polygon>
<svg-polygon sides="9" fill="yellow"></svg-polygon>
<svg-polygon sides="10" fill="blue"></svg-polygon>
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
customElements.define("svg-polygon", class extends HTMLElement {
connectedCallback() {
var radians = (deg) => (Math.PI * deg) / 180;
var sides = +this.getAttribute("sides") || 8;
var radius = +this.getAttribute("radius") || 200;
var vb = 2*radius + (this.getAttribute("padding") || 50);
var points = Array(sides).fill(radians(90-((180-(360/sides))/2)))
.map((offset,idx) => [
vb/2 + radius*Math.cos(offset+radians((360/sides)*idx) + Math.PI/2),
vb/2 + radius*Math.sin(offset+radians((360/sides)*idx) + Math.PI/2)
]);
this.innerHTML = `<svg viewBox="0 0 ${vb} ${vb}">` +
`<polygon points="${points.join(" ")}" fill="${this.getAttribute("fill")}"/>` +
`</svg>`;
}
})
<!-- language: lang-css -->
svg { width:180px; background:pink }
<!-- language: lang-html -->
<svg-polygon sides="8" fill="red"></svg-polygon>
<svg-polygon sides="9" fill="yellow"></svg-polygon>
<svg-polygon sides="10" fill="blue"></svg-polygon>
<!-- end snippet -->
答案3
得分: 1
Varun Vachhar的codepen "SVG多边形生成器"已经包含了一个用于调整起始角度的参数。要更改offsetDeg
(在pts(sideCount, radius)
中)的值,可以这样做:
let offsetDeg = (180 - angle) / 2;
生成器根据当前半径值计算多边形顶点。因此,所有顶点都位于一个圆上。但viewBox
不会自动根据渲染的多边形调整宽度和高度。
要实现裁剪多边形SVG,可以采取以下两种方法:
- 在渲染多边形后通过
getBBox()
重新计算viewBox
- 或者通过查找x和y的极值来重新计算多边形坐标以找到适当的偏移量
示例1:通过调整viewBox来裁剪
// 在渲染多边形后,通过以下代码调整viewBox
let bb = polygonEl.getBBox();
svgEl.setAttribute('viewBox', `${bb.x} ${bb.y} ${bb.width} ${bb.height}`);
示例2:通过重新计算多边形坐标来裁剪
// 计算多边形坐标后,通过以下代码找到x和y的极值
let xVals = polyPoints.map(pt => pt[0]);
let yVals = polyPoints.map(pt => pt[1]);
let left = Math.min(...xVals);
let right = Math.max(...xVals);
let top = Math.min(...yVals);
let bottom = Math.max(...yVals);
let width = right - left;
let height = bottom - top;
此外,上述示例还将多边形点转换为路径数据字符串。通常,SVG <path>
元素提供了更简洁的表示法,因为它们还支持相对命令并可以组合成复合路径。要将<polygon>
元素轻松转换为等效的<path>
,只需在points
属性前添加"M",并追加"z"命令字母:
<polygon points="244.949 386.37 141.421 386.37 51.764 334.607 0 244.949 0 141.421 51.764 51.764 141.421 0 244.949 0 334.607 51.764 386.37 141.421 386.37 244.949 334.607 334.607"/>
等同于:
<path d="M 244.949 386.37 141.421 386.37 51.764 334.607 0 244.949 0 141.421 51.764 51.764 141.421 0 244.949 0 334.607 51.764 386.37 141.421 386.37 244.949 334.607 334.607 z"/>
偏移值通过循环遍历点数组来计算,如下所示:
// 找到x和y的极值以裁剪和调整多边形和viewBox
let xVals = polyPoints.map(pt => pt[0]);
let yVals = polyPoints.map(pt => pt[1]);
let left = Math.min(...xVals);
let right = Math.max(...xVals);
let top = Math.min(...yVals);
let bottom = Math.max(...yVals);
let width = right - left;
let height = bottom - top;
英文:
Varun Vachhar's codepen "SVG Polygon Generator"
already includes a parameter to adjust the starting angle.
Change the offsetDeg
(in pts(sideCount, radius)
) value like so:
let offsetDeg = (180 - angle) / 2;
The generator calculates polygon vertices according to the current radius value. So all vertices will be on a circle. But the viewBox
won't automatically adjust width and height according to the rendered polygon.
To achieve a cropped polygon svg you can either:
- recalculate the
viewBox
viagetBBox()
after rendering the polygon - or recalculate the polygon coordinates by finding x and y extrema to find appropriate offsets
Example 1: crop via viewBox adjustment
<!-- begin snippet: js hide: true console: true babel: false -->
<!-- language: lang-js -->
const sideCountEl = document.querySelector('#js-side-count');
const radiusEl = document.querySelector('#js-radius');
const cxEl = document.querySelector('#js-cx');
const cyEl = document.querySelector('#js-cy');
const generateEl = document.querySelector('#js-generate');
const polygonEl = document.querySelector('#js-polygon');
const resultEl = document.querySelector('#js-result');
const svgEl = document.querySelector('#jsSvg');
function pts(sideCount, radius) {
const angle = 360 / sideCount;
const vertexIndices = range(sideCount);
let offsetDeg = (180 - angle) / 2;
const offset = degreesToRadians(offsetDeg);
return vertexIndices.map(index => {
return {
theta: offset + degreesToRadians(angle * index),
r: radius
};
});
}
function range(count) {
return Array.from(Array(count).keys());
}
function degreesToRadians(angleInDegrees) {
return Math.PI * angleInDegrees / 180;
}
function polygon([cx, cy], sideCount, radius) {
return pts(sideCount, radius).
map(({
r,
theta
}) => [
cx + r * Math.cos(theta),
cy + r * Math.sin(theta)
]);
}
function generatePolygon() {
const sideCount = +sideCountEl.value;
const radius = +radiusEl.value;
const s = 2 * radius;
let polyPoints = polygon(展开收缩, sideCount, radius);
polygonEl.setAttribute('points', polyPoints.flat().join(' '));
// crop by - adjust viewBox to current bbox
let bb = polygonEl.getBBox();
svgEl.setAttribute('viewBox', `${bb.x} ${bb.y} ${bb.width} ${bb.height}`);
// show output
resultEl.value = new XMLSerializer().serializeToString(jsSvg);
}
window.onload = generatePolygon;
// Listen to changes in <input />
let inputs = document.querySelectorAll('input');
inputs.forEach(input => {
input.addEventListener('input', e => generatePolygon());
});
// convert to relative path data
function polyPointsToPathRelative(polyPoints, precision) {
let pointsRel = [];
let offXrel = 0;
let offYrel = 0;
polyPoints.forEach(pt => {
let [x, y] = [pt[0] - offXrel, pt[1] - offYrel];
pointsRel.push(x, y)
offXrel += x;
offYrel += y;
});
// round
pointsRel = pointsRel.map(val => {
return +val.toFixed(precision)
});
let M = pointsRel.splice(0, 2);
let pathData = 'M' + M.join(' ') + 'l' + pointsRel.join(' ') + 'z';
return pathData;
}
<!-- language: lang-css -->
body {
margin: 0;
font-family: sans-serif;
}
input,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
body,
div,
fieldset,
form,
input[type=number],
legend,
main,
textarea {
box-sizing: border-box;
}
.ba {
border-style: solid;
border-width: 1px;
}
.bb {
border-bottom-style: solid;
border-bottom-width: 1px;
}
.b--dark-gray {
border-color: #333;
}
.b--transparent {
border-color: transparent;
}
.bw2 {
border-width: .25rem;
}
.db {
display: block;
}
.flex {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
.min-vh-100 {
min-height: 100vh;
}
.tracked {
letter-spacing: .1em;
}
.mw7 {
max-width: 48rem;
}
.w-100 {
width: 100%;
}
.pa0 {
padding: 0;
}
.pa2 {
padding: .5rem;
}
.pa3 {
padding: 1rem;
}
.pa4 {
padding: 2rem;
}
.pv2 {
padding-top: .5rem;
padding-bottom: .5rem;
}
.ph0 {
padding-left: 0;
padding-right: 0;
}
.mb0 {
margin-bottom: 0;
}
.mb2 {
margin-bottom: .5rem;
}
.mb4 {
margin-bottom: 2rem;
}
.mt5 {
margin-top: 4rem;
}
.mv4 {
margin-top: 2rem;
margin-bottom: 2rem;
}
.mh0 {
margin-left: 0;
margin-right: 0;
}
.ttu {
text-transform: uppercase;
}
.f5 {
font-size: 1rem;
}
.measure-wide {
max-width: 34em;
}
.center {
margin-right: auto;
margin-left: auto;
}
.flex-row-ns {
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
}
.mr4-ns {
margin-right: 2rem;
}
.mb0-ns {
margin-bottom: 0;
}
legend,
label {
text-transform: uppercase;
font-weight: 700;
}
pre {
overflow-wrap: break-word;
}
svg {
overflow: visible !important;
border: 1px solid #ccc;
}
textarea {
display: block;
width: 100%;
min-height: 20em;
white-space: pre-wrap !important;
white-space: pre-line !important;
}
<!-- language: lang-html -->
<main class="pa4 dark-gray mw7 center sans-serif min-vh-100 flex flex-column flex-row-ns ">
<form class="measure-wide mr4-ns mb4 mb0-ns f5">
<fieldset class="ba b--transparent pa0 mh0">
<legend class="f5 b ph0 mh0 ttu tracked bb bw2 pv2 db w-100">
Polygon Generator
</legend>
<div class="mt5 mb4">
<label class="f5 ttu tracked db b mb2" for="js-side-count">
Number of Sides
</label>
<input class="b pa2 input-reset ba bw2 bg-white b--dark-gray w-100" type="number" id="js-side-count" min="3" value="8">
</div>
<div class="mv4">
<label class="f5 ttu tracked db b mb2" for="js-radius">
Radius
</label>
<input class="b pa2 input-reset ba bw2 bg-white b--dark-gray w-100" type="number" id="js-radius" value="200">
</div>
</fieldset>
</form>
<div class="flex-auto flex items-center justify-center w-100">
<div class="w-100">
<svg class="w-100 white debug-grid mb4 db" viewBox="0 0 800 800" stroke="#111" fill="none" stroke-width="4" id="jsSvg">
<polygon id="js-polygon" points="" />
</svg>
<textarea id="js-result" class="code pa3 bg-near-white mb0 overflow-scroll"></textarea>
</div>
</div>
</main>
<!-- end snippet -->
Example 2: crop by recalculating polygon coordinates
<!-- begin snippet: js hide: true console: true babel: false -->
<!-- language: lang-js -->
const sideCountEl = document.querySelector('#js-side-count');
const radiusEl = document.querySelector('#js-radius');
const generateEl = document.querySelector('#js-generate');
const polygonEl = document.querySelector('#js-polygon');
const resultEl = document.querySelector('#js-result');
const svgEl = document.querySelector('#jsSvg');
function pts(sideCount, radius) {
const angle = 360 / sideCount;
const vertexIndices = range(sideCount);
let offsetDeg = (180 - angle) / 2;
const offset = degreesToRadians(offsetDeg);
return vertexIndices.map(index => {
return {
theta: offset + degreesToRadians(angle * index),
r: radius
};
});
}
function range(count) {
return Array.from(Array(count).keys());
}
function degreesToRadians(angleInDegrees) {
return Math.PI * angleInDegrees / 180;
}
function polygon([cx, cy], sideCount, radius) {
return pts(sideCount, radius).
map(({
r,
theta
}) => [
cx + r * Math.cos(theta),
cy + r * Math.sin(theta)
]);
}
function generatePolygon() {
const sideCount = +sideCountEl.value;
const radius = +radiusEl.value;
const s = 2 * radius;
// calculate polygont points
let polyPoints = polygon(展开收缩, sideCount, radius);
/**
* find x and y extrema
* to crop and pan polygon and viewBox
*/
let xVals = polyPoints.map(pt => {
return pt[0]
});
let yVals = polyPoints.map(pt => {
return pt[1]
});
let left = Math.min(...xVals);
let right = Math.max(...xVals);
let top = Math.min(...yVals);
let bottom = Math.max(...yVals);
let width = right - left;
let height = bottom - top;
// round coordinates
let precision = +inputPrecision.value;
// calculate cropped polygon
polyPoints = polyPoints.map(pt => {
return [+(pt[0] - left).toFixed(precision), +(pt[1] - top).toFixed(precision)];
});
// create relative path
let pathData = polyPointsToPathRelative(polyPoints, precision)
// shift points - adjust viewBox
svgEl.setAttribute('viewBox', `0 0 ${+width.toFixed(precision)} ${+height.toFixed(precision)}`);
polygonEl.setAttribute('d', pathData);
let output = new XMLSerializer().serializeToString(jsSvg).
replace(/\s{2,}/g, " ").
replaceAll('> <', '><').
replaceAll('<', '\n<');
//console.log(output);
resultEl.value = output;
}
window.onload = generatePolygon;
// Listen to changes in <input />
let inputs = document.querySelectorAll('input');
inputs.forEach(input => {
input.addEventListener('input', e => generatePolygon());
});
// convert to relative path data
function polyPointsToPathRelative(polyPoints, precision) {
let pointsRel = [];
let offXrel = 0;
let offYrel = 0;
polyPoints.forEach(pt => {
let [x, y] = [pt[0] - offXrel, pt[1] - offYrel];
pointsRel.push(x, y)
offXrel += x;
offYrel += y;
});
// round
pointsRel = pointsRel.map(val => {
return +val.toFixed(precision)
});
let M = pointsRel.splice(0, 2);
let pathData = 'M' + M.join(' ') + 'l' + pointsRel.join(' ') + 'z';
return pathData;
}
<!-- language: lang-css -->
body {
margin: 0;
font-family: sans-serif;
}
input,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
body,
div,
fieldset,
form,
input[type=number],
legend,
main,
textarea {
box-sizing: border-box;
}
.ba {
border-style: solid;
border-width: 1px;
}
.bb {
border-bottom-style: solid;
border-bottom-width: 1px;
}
.b--dark-gray {
border-color: #333;
}
.b--transparent {
border-color: transparent;
}
.bw2 {
border-width: .25rem;
}
.db {
display: block;
}
.flex {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
.min-vh-100 {
min-height: 100vh;
}
.tracked {
letter-spacing: .1em;
}
.mw7 {
max-width: 48rem;
}
.w-100 {
width: 100%;
}
.pa0 {
padding: 0;
}
.pa2 {
padding: .5rem;
}
.pa3 {
padding: 1rem;
}
.pa4 {
padding: 2rem;
}
.pv2 {
padding-top: .5rem;
padding-bottom: .5rem;
}
.ph0 {
padding-left: 0;
padding-right: 0;
}
.mb0 {
margin-bottom: 0;
}
.mb2 {
margin-bottom: .5rem;
}
.mb4 {
margin-bottom: 2rem;
}
.mt5 {
margin-top: 4rem;
}
.mv4 {
margin-top: 2rem;
margin-bottom: 2rem;
}
.mh0 {
margin-left: 0;
margin-right: 0;
}
.ttu {
text-transform: uppercase;
}
.f5 {
font-size: 1rem;
}
.measure-wide {
max-width: 34em;
}
.center {
margin-right: auto;
margin-left: auto;
}
.flex-row-ns {
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
}
.mr4-ns {
margin-right: 2rem;
}
.mb0-ns {
margin-bottom: 0;
}
legend,
label {
text-transform: uppercase;
font-weight: 700;
}
pre {
overflow-wrap: break-word;
}
svg {
overflow: visible !important;
border: 1px solid #ccc;
}
textarea {
display: block;
width: 100%;
min-height: 20em;
white-space: pre-wrap !important;
white-space: pre-line !important;
}
<!-- language: lang-html -->
<main class="pa4 dark-gray mw7 center sans-serif min-vh-100 flex flex-column flex-row-ns ">
<form class="measure-wide mr4-ns mb4 mb0-ns f5">
<fieldset class="ba b--transparent pa0 mh0">
<legend class="f5 b ph0 mh0 ttu tracked bb bw2 pv2 db w-100">
Polygon Generator
</legend>
<div class="mt5 mb4">
<label class="f5 ttu tracked db b mb2" for="js-side-count">
Number of Sides
</label>
<input class="b pa2 input-reset ba bw2 bg-white b--dark-gray w-100" type="number" id="js-side-count" min="3" value="8">
</div>
<div class="mv4">
<label class="f5 ttu tracked db b mb2" for="js-radius">
Radius
</label>
<input class="b pa2 input-reset ba bw2 bg-white b--dark-gray w-100" type="number" id="js-radius" value="200">
</div>
<div class="mv4">
<label class="f5 ttu tracked db b mb2" for="js-radius">
precision
</label>
<input class="b pa2 input-reset ba bw2 bg-white b--dark-gray w-100" type="number" id="inputPrecision" value="2">
</div>
</fieldset>
</form>
<div class="flex-auto flex items-center justify-center w-100">
<div class="w-100">
<svg viewBox="0 0 800 800" id="jsSvg">
<path id="js-polygon" d="" />
</svg>
<textarea id="js-result" class="code pa3 bg-near-white mb0 overflow-scroll"></textarea>
</div>
</div>
</main>
<!-- end snippet -->
The above example also converts polygon points to a path data string.
Usually, svg <path>
elements offer a more concise notation – as they also support relative commands and can also be combined as compound paths.
You can easily convert a <polygon>
element to a <path>
equivalent by simply prepending a "M" and appending a "z" command letter to the points
attribute.
<polygon points="244.949 386.37 141.421 386.37 51.764 334.607 0 244.949 0 141.421 51.764 51.764 141.421 0 244.949 0 334.607 51.764 386.37 141.421 386.37 244.949 334.607 334.607"/>
equals
<path d="M 244.949 386.37 141.421 386.37 51.764 334.607 0 244.949 0 141.421 51.764 51.764 141.421 0 244.949 0 334.607 51.764 386.37 141.421 386.37 244.949 334.607 334.607 z"/>
The offset values are calculated by looping through the point array like this:
/**
* find x and y extrema
* to crop and pan polygon and viewBox
*/
let xVals = polyPoints.map(pt => {
return pt[0]
});
let yVals = polyPoints.map(pt => {
return pt[1]
});
let left = Math.min(...xVals);
let right = Math.max(...xVals);
let top = Math.min(...yVals);
let bottom = Math.max(...yVals);
let width = right - left;
let height = bottom - top;
// round coordinates
let precision = +inputPrecision.value;
// calculate cropped polygon
polyPoints = polyPoints.map(pt => {
return [+(pt[0] - left).toFixed(precision), +(pt[1] - top).toFixed(precision)];
});
// shift points - adjust viewBox
svgEl.setAttribute('viewBox', `0 0 ${+width.toFixed(precision)} ${+height.toFixed(precision)}`);
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论