Is it possible to only show half of a SVG icon? - javascript

I currently have a rating system that produces 5 stars that can show half values. I did this by using FontAwesome's Half Stars and did some CSS tricks to make them look like one star. But I was thinking of increasing my React and CSS knowledge by coming up with a way to show only half of an SVG icon. So instead of using the Half Stars, I could use whatever Icon the user wanted, and it would only show 50% of the Icon for example if you wanted to give a 3.5 rating.
Q: Can you show only half of an Icon and somehow know if the user is clicking on one side or the other?
Here is the code I have currently that uses the HalfStars for a little bit of reference
import React, { useState } from 'react'
import { FaRegStarHalf, FaStarHalf } from 'react-icons/fa'
import './styles/Rater.css'
const Rater = ({ iconCount }) => {
const [rating, setRating] = useState(null)
const [hover, setHover] = useState(null)
// check if user has set a rating by clicking a star and use rating to determine icons
const Star = rating ? FaStarHalf : FaRegStarHalf
return (
<div>
{[...Array(iconCount), ...Array(iconCount)].map((icon, i) => {
const value = (i + 1) / 2
return (
<label>
<input
type='radio'
name='rating'
value={value}
onClick={() => {
console.log(`value => `, value)
return setRating(value)
}}
/>
<div className='star-container'>
<div>
<Star
className={i % 2 ? 'star-left' : 'star'}
color={value <= (hover || rating) ? '#ffc107' : '#e4e5e9'}
onMouseEnter={() => setHover(value)}
onMouseLeave={() => setHover(null)}
/>
</div>
</div>
</label>
)
})}
</div>
)
}
export default Rater

I have written a code to get you the idea; If you click on the right side of the star, its color changes to blue and if you click on the left side, its color changes to gold. Also, it's better to not use stopPropagation and check e.target of the event.
const starIcon = document.getElementById("star");
const icon = document.getElementById("icon");
starIcon.onclick = e => {
starIcon.style.color = "blue";
e.stopPropagation();
};
icon.onclick = e => {
starIcon.style.color = "gold";
}
i {
clip-path: inset(0 0 0 50%);
color: gold;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" rel="stylesheet">
</head>
<body>
<span id="icon"><i id="star", class="fas fa-star"></i></span>
</body>
</html>

You can do it in ONE SVG
I ditched the Font-Awesome icon
searched for "star" in the 7000+ icons on https://iconmeister.github.io/ (first load takes a minute)
Picked the star icon with the best d-path (Clarity Iconset: cl-social-star-solid)
copied only the d-path
Edited the d-path in https://yqnn.github.io/svg-path-editor/ to a 100x100
viewBox/grid
made it an inverse star by prepending M0 0h100v100h-100v-100 to the path
Created a new SVG file in a 0 0 300 100 viewBox to fit 3 stars.. see below
Added a background rectangle setting gold color rating with width="50%"
Used 3 inverse stars, each at an x-offset
added 6 rectangles covering all half-stars
set inline events on every "half-star"
(the click works in this SO snippet, but SO adds a long delay)
Proof of Concept
<svg viewBox="0 0 300 100" width="500px">
<rect id="rating" width="50%" fill="gold" height="100%" />
<path id="star" fill="green"
d="M0 0h100v100h-100v-100m91 42a6 6 90 00-4-10l-22-1a1 1 90 01-1
0l-8-21a6 6 90 00-11 0l-8 21a1 1 90 01-1 1l-22 1a6 6 90 00-4
10l18 14a1 1 90 010 1l-6 22a6 6 90 008 6l19-13a1 1 90 011 0l19
13a6 6 90 006 0a6 6 90 002-6l-6-22a1 1 90 010-1z"/>
<use href="#star" x="100" />
<use href="#star" x="200" />
<rect id="c" width="16.66%" height="100%" fill="transparent" stroke="red"
onclick="console.log(this)" />
<use href="#c" x="50" />
<use href="#c" x="100" />
<use href="#c" x="150" />
<use href="#c" x="200" />
<use href="#c" x="250" />
</svg>
A Star Rating Component <star-rating stars=N >
You don't want to create all this SVG by hand... couple lines of JavaScript can create the SVG, for any number of stars
Using a W3C standard Web Component here, because it runs in this page and is not as complex as a React Component.
https://developer.mozilla.org/en-US/docs/Web/Web_Components
Not using <use>, just duplicate all paths and rects with a x offset
mouseover events set the background % color
click shows the index of the clicked halfstar (0+)
Rating can be set with values or percentage; document.querySelector('[stars="5"]').rating="90%" (4.5 stars)
needs extra work for your use case
All required HTML & JavaScript:
<star-rating stars=5 rating="3.5"
bgcolor="green" nocolor="grey" color="gold"></star-rating>
<star-rating stars=7 rating="50%"
bgcolor="rebeccapurple" nocolor="beige" color="goldenrod"></star-rating>
<script>
document.addEventListener("click", (evt) => console.log(evt.target.getAttribute("n")))
customElements.define("star-rating", class extends HTMLElement {
set rating( rate ) {
if (!String(rate).includes("%")) rate = Number(rate) / this.stars * 100 + "%";
this.querySelector("#rating").setAttribute("width", rate);
}
connectedCallback() {
let { bgcolor, stars, nocolor, color, rating } = this.attributes;
this.stars = ~~stars.value || 5;
this.innerHTML =
`<svg viewBox="0 0 ${this.stars*100} 100" style="cursor:pointer;width:300px">`
+ `<rect width="100%" height="100" fill="${nocolor.value}"/>`
+ `<rect id="rating" height="100" fill="${color.value}" />`
+ Array( this.stars ).fill()
.map((i, n) => `<path fill="${bgcolor.value}" d="M${ n*100 } 0h102v100h-102v-100m91 42a6 6 90 00-4-10l-22-1a1 1 90 01-1 0l-8-21a6 6 90 00-11 0l-8 21a1 1 90 01-1 1l-22 1a6 6 90 00-4 10l18 14a1 1 90 010 1l-6 22a6 6 90 008 6l19-13a1 1 90 011 0l19 13a6 6 90 006 0a6 6 90 002-6l-6-22a1 1 90 010-1z"/>`)
.join("")
+ Array( this.stars * 2 ).fill()
.map((i, n) => `<rect x="${ n*50 }" n="${n}" opacity="0" width="50" height="100"`
+ ` onclick="dispatchEvent(new Event('click'))" `
+ ` onmouseover="this.closest('star-rating').rating = ${(n+1)/2}"/>`)
.join("")
+ "</svg>";
this.rating = rating.value;
}
});
</script>
Notes:
This native <star-rating> Component (also called Custom Element because NO shadowDOM is involved) has ZERO dependencies
no libraries
no external SVG
native components are not self-closing tags and must contain a hyphen, so notation is: <star-rating></star-rating>
Changed the star to M0 0h102v100h-102v-100 (2 pixel overlap) to cover SVG rounding issues
Supported in all Frameworks... except...
React doesn't support this modern W3C Web Components Standard yet.
React scores just 71% on https://custom-elements-everywhere.com/
All other Frameworks (Angular, Vue, Svelte) have 100% support
You have to do some extra work to handle native DOM Elements and Events in React; but the Custom Element isn't complex.. it creates SVG; should be easy to replicate as a React Component.

Here is a different idea using 3D transformation:
i {
color: gold;
}
.container {
background: #fff;
transform-style: preserve-3d;
}
.half {
transform: rotateY(1deg);
}
i.fa-star:hover {
color:red;
}
<link href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" rel="stylesheet">
<div class="container">
<i id="star" class="fas fa-star fa-5x"></i>
<i id="star" class="fas fa-star fa-5x"></i>
<i id="star" class="fas fa-star fa-5x"></i>
<i id="star" class="fas fa-star fa-5x half"></i>
</div>
<div class="container">
<i id="star" class="fas fa-star fa-5x"></i>
<i id="star" class="fas fa-star fa-5x half"></i>
</div>

The simplest solution I can think of is to have a row of the svg icons and then use overflow:hidden to clip up to the user's selection.
Then using half the width of the icon, it can go by half increments.
$(document).ready(function()
{
$(document).on("mousemove", ".star-container.unvoted", function(e)
{
let width = $(this).find(".star").width() / 2;
e.clientX = Math.ceil(e.clientX /= (width)) * width;
$(this).find(".inner").css("width", e.clientX + "px");
});
$(".star-container.unvoted").click(function(e)
{
$(this).removeClass("unvoted");
});
});
.star-container .inner{
overflow: hidden;
width:100%;
height:100%;
}
.star-container .inner .star{
display:inline-block;
width:20px;
height:20px;
background-image:url('https://www.flaticon.com/svg/static/icons/svg/1828/1828884.svg');
background-size:contain;
}
.star-container .inner .inner-2{
width:10000px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="star-container unvoted">
<div class="inner">
<div class="inner-2">
<div class="star"></div><div class="star"></div><div class="star"></div><div class="star"></div><div class="star"></div>
</div>
</div>
</div>

Related

Vue3 Move/drag image vertically

I have an image inside a wrapper div. I want to move the image vertically inside the div because the image does not fit.
I found a few libs that do this in react but not many resources for Vue
I want to achieve a functionality like in Google Maps, where you can drag the map around. The map view always stays inside the wrapping container and the move extends only until the image top/bottom is reached
<script setup>
...
onMounted(() => {
let bgDiv = document.getElementById("test");
bgDiv.addEventListener('mousemove', e => {
let y = e.offsetY;
bgDiv.style.cssText = `transform: translate3d(0, ${y}px, 0)`;
});
})
</script>
<div class="test" id="test">
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 1920 1080"
preserveAspectRatio="xMinYMin meet"
class="svg-content">
<!-- overhead appears when viewbox > image, otherwise should be fine, so just make them match/fit -->
<image
width="1920"
height="1080"
xlink:href="#/assets/background.gif"></image>
<a class="cursor-pointer" data-bs-toggle="modal" data-bs-target="#">
<rect
x="618"
y="140"
fill="#fff"
opacity="0.5"
width="155"
height="940"/>
</a>
</svg>
</div>

How we can reduce the number of svg icons inside React project?

I am working on a project which have around 100 icons inside the assets/icons folder. Some icons are quite similar such as trash(delete) icon with blue and red colour, and check icon with three different colour.
How we can pass the colour as props inside svg file. So that we can reduce the number of icon as much as possible.
Note : We don't want to make specific react component and pass the colour as props.
Here is an working example of my code. className="fill-red" will fill the colour in whole <svg /> . But how we can fill the colour in it's nested element like in <path/> or might be <rect/>
import { ReactComponent as Cross } from 'assets/icons/cross.svg';
import React from 'react';
const Alert = (props) => {
return (
<div>
<div className="flex items-start">
<Error className="mr-2 fill-red" />
{props.label}
</div>
</div>
);
};
export default Alert;
Here is my cross.svg
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.205 4.71967C15.4978 5.01256 15.4978 5.48744 15.205 5.78033L11.023 9.96229L15.205 14.1443C15.4979 14.4372 15.4979 14.9121 15.205 15.205C14.9121 15.4978 14.4372 15.4978 14.1443 15.205L9.96233 11.0229L5.78033 15.205C5.48744 15.4978 5.01256 15.4978 4.71967 15.205C4.42678 14.9121 4.42678 14.4372 4.71967 14.1443L8.90167 9.96229L4.71971 5.78033C4.42682 5.48744 4.42682 5.01256 4.71971 4.71967C5.0126 4.42678 5.48748 4.42678 5.78037 4.71967L9.96233 8.90163L14.1443 4.71967C14.4372 4.42678 14.9121 4.42678 15.205 4.71967Z"/>
</svg>

What should I use to show live icons in React OR Javascript? Like how much coffee is filled in a mug?

I want to create dashboard with lots of live icons. Live icons means icons content will change dynamically.
For example:- Let say i have an icon of a coffee cup, based upon number of coffee sold the coffee level in cup goes up.
I tried solving it using SVG & CSS. Below is the live code-sandbox DEMO example. (same code I am posting below)
https://codesandbox.io/s/fill-coffee-to-svg-1sfb0
what exactly I am doing is on top of SVG component i using div node and applying css on the div tag.
Issue is that, It is very difficult to manage path/points dynamically in SVG, also for different screen version it will be difficult.
Can anyone suggest any better approach for this.
import ReactDOM from "react-dom";
import React, { Component } from "react";
class Todo extends Component {
state = { height: 4 };
setHeight = (e, fillType) => {
e.preventDefault();
console.log(this.state.height);
if (this.state.height < 100) {
if (fillType === "fill") {
this.setState({
height: this.state.height + 2
});
}
}
if (this.state.height >= 2) {
if (fillType === "drink") {
this.setState({
height: this.state.height - 2
});
}
}
};
render() {
return (
<div>
<svg width="255" height="224" xmlns="http://www.w3.org/2000/svg">
<g>
<rect
fill="#fff"
id="canvas_background"
height="226"
width="257"
y="-1"
x="-1"
/>
<g
display="none"
overflow="visible"
y="0"
x="0"
height="100%"
width="100%"
id="canvasGrid"
>
<rect
fill="url(#gridpattern)"
stroke-width="0"
y="0"
x="0"
height="100%"
width="100%"
/>
</g>
</g>
<g>
<title>Layer 1</title>
<path
stroke="#000"
id="svg_1"
d="m78.82963,176.75921l97.93778,0c14.11708,-0.03733 23.74788,-17.00704 23.70086,-34.46505l0,-11.73873c27.94999,-0.03136 48.22814,-30.02253 48.21769,-64.99381c0.01045,-35.59398 -19.86965,-64.83701 -43.00946,-64.81162l-150.95938,0l0,141.54714c0.02194,19.22158 11.60543,34.42772 24.1125,34.46207zm121.63551,-149.38391l0,0l5.20823,0c19.14875,-0.04331 25.29102,25.83983 25.19908,38.38045c0.01881,20.24897 -10.47393,39.66916 -30.40731,37.78463l0,-76.16508zm-199.71514,158.00316c0.01776,26.16387 13.9729,38.29683 25.20535,38.37149l202.59351,0c13.39827,-0.07466 25.14161,-15.13147 25.20117,-38.37149l-253.00002,0z"
stroke-width="1.5"
fill="#fff"
/>
</g>
</svg>
<div
style={{
position: "absolute",
left: "63px",
top: "9px",
height: "175px",
width: "145px",
borderBottomLeftRadius: "25px",
borderBottomRightRadius: "25px",
overflow: "auto"
}}
>
<div
style={{
height: this.state.height + "%",
width: "100%",
position: "absolute",
bottom: "0",
zIndex: 1000,
background: "green",
transition: "all .4s"
}}
/>
</div>
<button onClick={e => this.setHeight(e, "fill")}>
fill more cofee +
</button>
<button onClick={e => this.setHeight(e, "drink")}>drink cofee -</button>
</div>
);
}
}
ReactDOM.render(<Todo />, document.getElementById("root"));
Have you tried using the react-spring library?
In the basics section of react-spring documentation (can be found here - https://www.react-spring.io/docs/hooks/basics) this code snippet is given:
const props = useSpring({ x: 100, from: { x: 0 } })
return (
<animated.svg strokeDashoffset={props.x}>
<path d="..." />
</animated.svg>
)
Using your example of your coffee mug, if you would render your mug separate to the coffee filling the mug, with the coffee stroke being upwards, then you could use this technique to animate the coffee height.
Edit:
As you are using a class component you will want to use the render props API for this - https://www.react-spring.io/docs/props/spring
<Spring
from={{ x: 100 }}
to={{ x: 0 }}>
{props => (
<svg strokeDashoffset={props.x}>
<path d="..." />
</svg>
)}
</Spring>

How can I animate the tracing a SVG rectangle using JavaScript?

For a game I'm working on,
I would like to be able to draw a SVG rectangle; using a percentage value (50% would draw half the rectangle stroke).
I need to do this in Javascript since I'll update the value quite often.
<svg id="rectangle-timer" style="width:100%;height:100%;">
<rect width="100%" height="100%"/>
</svg>
I saw quite nice JS libraries like drawSVG or Vivus, but it seems that they work with paths, not with basic shapes like rectangles.
Can anyone help ?
Thanks.
The reason most libraries will use path elements is because of their inheritance from the SVGGeometryElement prototype, which gives you handy functions for computing the path length. So if we swap out this rectangle for a path like this:
<path d="M 0 0 L 1 0 L 1 1 L 0 1 z" />
We get exactly the same output, but its much more controllable. After that, we can just adjust the strokeDasharray value in the style to extend and remove some stroke. For this property we just need two values: initial dash size and initial empty space. So when our progress is 0, we want the first value to be 0 and the second to be the path length , and as we approach 1 we want the second value to 0 and the first one to increase to the path length.
function update( amount ){
const total = rect.getTotalLength();
const filled = total * amount;
const none = total - filled;
rect.style.strokeDasharray = `${filled} ${none}`;
}
const rect = document.getElementById( 'rectangle' );
const input = document.getElementById( 'input' );
input.addEventListener( 'mousemove', event => update( input.value ));
update( input.value );
<svg width="200px" height="200px" viewBox="0 0 200 200">
<path d="M 20 20 L 180 20 L 180 180 L 20 180 z" id="rectangle" fill="none" stroke="black" stroke-width="10" />
</svg>
<input id="input" type="range" min="0" max="1" step=".01" />
If you insist on using a rect, you could get a rectangle's path length by taking its width and height twice, which would look something like this:
function update( amount ){
const total = rect.width.baseVal.value * 2 + rect.height.baseVal.value * 2;
const filled = total * amount;
const none = total - filled;
rect.style.strokeDasharray = `${filled} ${none}`;
}
const rect = document.getElementById( 'rectangle' );
const input = document.getElementById( 'input' );
input.addEventListener( 'mousemove', event => update( input.value ));
update( input.value );
<svg width="200px" height="200px" viewBox="0 0 200 200">
<rect x="20" y="20" width="160" height="160" id="rectangle" fill="none" stroke="black" stroke-width="10" />
</svg>
<input id="input" type="range" min="0" max="1" step=".01" />
In the long run, however, this would mean less versatility, so I would suggest switching to path.
This is my solution: The SVG has preserveAspectRatio ="none" style="width:100%;height:100vh;" The total length of the path is 2*window.innerWidth + 2*window.innerHeight; Both stroke-dasharray and stroke-dashoffset are igual to the total length of the path.
I'm using an input type="range" to animate the stroke-dashoffset.
In order to preserve the stroke width and avoid stretching I'm using vector-effect="non-scaling-stroke"
I hope this is what you need.
function timer(){
let totalLength = 2*window.innerWidth + 2*window.innerHeight;
thePath.setAttributeNS(null, "style", `stroke-dashoffset:${totalLength * (1-range.value)}`)
}
timer()
range.addEventListener("input",timer);
setTimeout(function() {
timer()
addEventListener('resize', timer, false);
}, 15);
*{margin:0; padding:0;}
#thePath {
stroke-dasharray: calc(2 * 100vw + 2* 100vh);
stroke-dashoffset: calc(2 * 100vw + 2* 100vh);
}
#rectangle-timer{background:#dfdfdf}
[type="range"] {
position: absolute;
display: block;
width: 200px;
height: 20px;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
<svg id="rectangle-timer" viewBox="0 0 100 100" preserveAspectRatio ="none" style="width:100%;height:100vh;">
<path id="thePath" d="M0,0L100,0 100,100 0,100 0,0" fill="none" stroke="skyBlue" stroke-width="25" vector-effect="non-scaling-stroke" />
</svg>
<input type="range" id="range" value=".5" min="0" max="1" step=".01" />

Vue.js event target is null?

I got a component with two styled buttons and one same event
"mouseEnter".
<template>
<div>
<a
class="button red"
href="/about"
#mouseover="mouseEnter">
<svg viewBox="0 0 180 60">
<path d="..."/>
</svg>
<span class="buttonSpan">About</span>
</a>
<a
class="button green"
href="/contact"
#mouseover="mouseEnter">
<svg viewBox="0 0 180 60">
<path d="..."/>
</svg>
<span class="buttonSpan">Contact</span>
</a>
</div
</template>
When the event triggers I want to do something with the path and the span of the button that is hovered.
I'm trying to reference them with event.target but for the span I get null, and for the path everything is working good.
methods: {
buttonEnter(event) {
const buttonPath = event.target.querySelector('path')
const buttonSpan = event.target.querySelector('span')
...
}
How should i reference the span ? Is there any other way I can do it ?
the event target will bubble up starting from the children of the node that you attached your listener to. Use event.currentTarget

Categories

Resources