Difference between canvas and SVG (fill effect) - javascript

I have a basic question about canvas and SVG. I want to create an overlay with holes and fill it with some color.
It seems to work using canvas, but I want to try SVG (to handle event e.g. resize).
canvas.width = 200;
canvas.height = 200;
var context = canvas.getContext('2d');
//fill background
context.globalAlpha = 0.8;
context.fillStyle = "blue"
context.fillRect(0, 0, 200, 200);
context.globalCompositeOperation = 'xor';
context.globalAlpha = 1.0;
context.fillStyle = "rgba(0,0,0,1)";
context.fillRect(50, 50, 50, 50);
https://jsfiddle.net/gpx21/195ygzhq/
but SVG mask looks too light.
<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="
position: absolute;
z-index: 19000;
top: 0;
left: 0;
">
<defs>
<mask id="mask1" maskContentUnits="userSpaceOnUse">
<rect x="0" y="0" width="200" height="200" style="opacity: 0.8; fill:blue;"></rect>
<path d="M50 50 H100 V100 H50Z" id="hole"></path>
</mask>
</defs>
<rect width="200" height="200" style="fill:blue; mix-blend-mode: darken;mask:url(#mask1);"></rect>
</svg>
https://jsfiddle.net/gpx21/1fktpnr5/
how can I archieve the same effect as in canvas? Thanks.

You can think of an SVG mask as a grayscale image. Where that image is white, the masked element is visible, and where it is black, the masked element is transparent.
So, to have a translucent blue <rect>, you can apply a <mask> that is mostly light gray (i.e. "almost white") except for a black part which will result in a transparent hole:
<defs>
<mask id="mask1" maskContentUnits="userSpaceOnUse">
<rect x="0" y="0" width="200" height="200" fill="lightgray"></rect>
<path d="M50 50 H100 V100 H50Z" id="hole" fill="black"></path>
</mask>
</defs>
<rect width="200" height="200" style="fill:blue; mask:url('#mask1');"></rect>
https://jsfiddle.net/1fktpnr5/1/

This is a simple fix. When you define masks in your svg documents, mask elements can be given a fill of black to hide them, a fill of white to display them, or something in between for varying levels of transparency. (source)
In your mask code:
<defs>
<mask id="mask1" maskContentUnits="userSpaceOnUse">
<rect x="0" y="0" width="200" height="200" style="opacity: 0.8; fill:blue;"></rect>
<path d="M50 50 H100 V100 H50Z" id="hole"></path>
</mask>
</defs>
You can change your fill:blue to fill:white and this should fix it.

Related

Fold corner of a SVG playingcard, revealing the other side

With some elbow grease I managed to create a 2D version of the effect:
But it feels contrived. I would like to animate this, change the size and the angle.
Before I start digging into maths to calc all those hardcoded coordinates...
Can this be done in a smarter way? (Without dependencies on 3rd party libraries)
<svg viewBox="0 0 200 278" style="height:180px">
<defs>
<pattern id="back" width="200" height="278" patternUnits="userSpaceOnUse">
<image width="200" href="https://svg-cdn.github.io/cm-back-red.svg" />
</pattern>
<pattern id="front" width="200" height="278" patternUnits="userSpaceOnUse">
<image width="200" href="https://svg-cdn.github.io/cm-hearts-king.svg" />
</pattern>
<clipPath id="clip">
<path d="M0 178L0 0h200v278h-59z" />
</clipPath>
</defs>
<g clip-path="url(#clip)">
<rect width="200" height="278" fill="url(#back)" />
<rect width="200" height="278" fill="url(#front)"
transform="translate(-100 139) rotate(-100 100 139)" />
</g>
</svg>
Version 1
From CCProg his answer, Heiko his answer takes some more work.
My math fails me again, how to make the card corner follow the mouse position. It now calculates the x and y offsets.
And a full card turn would be cool
customElements.define("fold-card", class extends HTMLElement {
constructor() {
super()
.attachShadow({mode:"open"})
.innerHTML = `<svg viewBox="0 0 300 378" style="height:95vh;cursor:hand;background:pink">
<defs><pattern id="back" width="200" height="278" patternUnits="userSpaceOnUse"><image width="200" href="https://svg-cdn.github.io/cm-back-red.svg" /></pattern>
<pattern id="front" width="200" height="278" patternUnits="userSpaceOnUse"><image width="200" href="https://svg-cdn.github.io/cm-hearts-king.svg" /></pattern>
<clipPath id="clip"><path /></clipPath></defs>
<g clip-path="url(#clip)">
<rect width="200" height="278" fill="url(#back)" />
<rect id="below" width="200" height="278" fill="url(#front)" />
</g></svg>`;
}
connectedCallback() {
let svg = this.shadowRoot.querySelector("svg");
let pt = svg.createSVGPoint();
let [x,y] = (this.getAttribute("crease")||"0,0").split(",");
this.fold({x,y});
this.onmousemove = evt =>{
pt.x = evt.clientX;
pt.y = evt.clientY;
this.fold( pt.matrixTransform(svg.getScreenCTM().inverse()) );
}
}
fold({x,y}){
if(x>=0 && x<=200 && y>=0 && y<=278) {
this.shadowRoot.querySelector('#clip path')
.setAttribute('d', `M0 ${y}L0 0H300V278H${x}z`);
let a = Math.atan2(x, 278 - y) * 180 / Math.PI;
this.shadowRoot.querySelector('#below')
.setAttribute('transform', `rotate(${-2*a} 0 ${y}) translate(-200)`);
}
}
})
<style> fold-card { height: 180px } </style>
<fold-card crease="200,10"></fold-card>
<fold-card crease="100,50"></fold-card>
Version 2 - almost no Math using <marker>
chrwahl his answer is cleverly using a <marker> to position the revealing card at the end of a rotating <line>
Work in progress: https://jsfiddle.net/WebComponents/r2y7x3fd/
Still need to calculate the position of the red dot, to create a <clip-path>
Don't fear the math, it is really quite simple.
Start with the two points where the "crease" meets the card sides, because you can choose them freely.
P1 = 0, 178 // x is fixed
P2 = 141, 278 // y is fixed
Compute the angle of the "crease" from the vertical in degrees:
a = Math.atan2((P2.x - P1.x), (P2.y - P1.y))*180/Math.PI
= Math.atan2(141, 100)*180/Math.PI
= 54.655
Move the second card to the left side of the y-axis
transform ="translate(-200)"
...and rotate it by -2a around P1
transform ="rotate(-109.31 0 178) translate(-200)"
That's all.
function crease (y1, x2) {
document.querySelector('#clip path')
.setAttribute('d', `M0 ${y1}L0 0H200V278H${x2}z`);
const a = Math.atan2(x2, 278 - y1)*180/Math.PI;
document.querySelector('#below')
.setAttribute('transform', `rotate(${-2*a} 0 ${y1}) translate(-200)`);
}
crease(178, 141);
<svg viewBox="0 0 200 278" style="height:180px">
<defs>
<pattern id="back" width="200" height="278" patternUnits="userSpaceOnUse">
<image width="200" href="https://svg-cdn.github.io/cm-back-red.svg" />
</pattern>
<pattern id="front" width="200" height="278" patternUnits="userSpaceOnUse">
<image width="200" href="https://svg-cdn.github.io/cm-hearts-king.svg" />
</pattern>
<clipPath id="clip">
<path />
</clipPath>
</defs>
<g clip-path="url(#clip)">
<rect width="200" height="278" fill="url(#back)" />
<rect id="below" width="200" height="278" fill="url(#front)" />
</g>
</svg>
Here is an animated solution that uses the CSS rotate3d transformation. I achieved it with quite some trial and error, and it is still not clear to me how exactly the transform-origin affects the animation.
I found that the animation changes when I move the transform-origin: 81px bottom; rule from div.back to input:checked~div.back.corner. Apparently, elements should have the same transform-origin before and after the animation for it to be smooth, even if the element before the animation is not transformed at all.
I have added a Javascript function that computes all the parameters from just the height and width and the x and y coordinates of the "crease".
var rules = document.styleSheets[0].rules;
function render(form) {
rules[1].styleMap.set("width", `${form.w.value}px`);
rules[1].styleMap.set("height", `${form.h.value}px`);
rules[2].styleMap.set("transform-origin", `${form.x.value}px bottom`);
rules[3].styleMap.set("clip-path", `path("M${form.w.value},${form.h.value} l${-form.x.value},0 l${form.x.value},${-form.y.value} l0,${form.y.value}z")`);
rules[3].styleMap.set("transform", `translateX(${form.x.value - form.w.value / 2}px) rotateY(180deg) translateX(${form.w.value / 2 - form.x.value}px)`);
rules[3].styleMap.set("transform-origin", `${form.w.value - form.x.value}px bottom`);
rules[4].styleMap.set("clip-path", `path("M0,0 l${form.w.value},0 l0,${form.h.value} l${form.x.value - form.w.value},0 l${-form.x.value},${-form.y.value} l0,${form.y.value-form.h.value}z")`);
rules[5].styleMap.set("clip-path", `path("M0,${form.h.value} l${form.x.value},0 l${-form.x.value},${-form.y.value} l0,${form.y.value}z")`);
rules[6].styleMap.set("transform", `rotate3d(${-form.x.value}, ${-form.y.value}, 0, -170deg)`);
rules[7].styleMap.set("transform", `translateX(${form.x.value - form.w.value / 2}px) rotateY(180deg) translateX(${form.w.value / 2 - form.x.value}px) rotate3d(${form.x.value}, ${-form.y.value}, 0, -170deg)`);
}
body {
position: relative;
}
div {
position: absolute;
width: 200px;
height: 278px;
transform-style: preserve-3d;
backface-visibility: hidden;
transition: 0.5s;
zoom: 0.5;
}
div.back {
background: url(https://svg-cdn.github.io/cm-back-red.svg);
transform-origin: 81px bottom;
}
div.front {
background: url(https://svg-cdn.github.io/cm-hearts-king.svg);
clip-path: path("M200,278 l-81,0 l81,-99 l0,99z");
transform: translateX(-19px) rotateY(180deg) translateX(19px);
transform-origin: 119px bottom;
}
div.nocorner {
clip-path: path("M0,0 l200,0 l0,278 l-119,0 l-81,-99 l0,-179z");
}
div.corner {
clip-path: path("M0,278 l81,0 l-81,-99 l0,99z");
}
input:checked~div.back.corner {
transform: rotate3d(-81, -99, 0, -170deg);
}
input:checked~div.front {
transform: translateX(-19px) rotateY(180deg) translateX(19px) rotate3d(81, -99, 0, -170deg);
}
<form onsubmit="render(this); return false;">
width <input name="w" size="3" value="200" />
height <input name="h" size="3" value="278" />
x axis <input name="x" size="3" value="81" />
y axis <input name="y" size="3" value="99" />
<input type="submit" value="Render" />
</form>
<input type="checkbox" /> Fold
<div class="back nocorner"></div>
<div class="back corner"></div>
<div class="front"></div>
Remark on the "interactive card folder": If the folded card corner is (x, bottom - y), then the crease goes from ((x²+y²)/2x, bottom) to (0, bottom - (x²+y²)/2y).
This is not perfect, but maybe you can work more in this direction. I really did some experimentation here.
The front card is a marker, making it follow the angle of the line that it is placed on. The line is then positions according to the mouse. The mask that cuts off the cards in the bottom is made with a line that has a wide (large) stroke and fixed in the bottom left corner.
let SVG = document.getElementById("SVG");
let LINE1 = document.getElementById("LINE1");
let LINE2 = document.getElementById("LINE2");
const toSVGPoint = (x, y) => (new DOMPoint(x, y)).matrixTransform(SVG.getScreenCTM().inverse());
SVG.addEventListener('mousemove', e => {
let p = toSVGPoint(e.clientX, e.clientY);
LINE1.setAttribute('x2', p.x);
LINE1.setAttribute('y2', p.y);
LINE1.setAttribute('x1', 0-p.x);
LINE1.setAttribute('y1', 278-p.x);
LINE2.setAttribute('x2', p.x);
LINE2.setAttribute('y2', p.y);
});
<svg id="SVG" viewBox="0 0 300 278" style="height:180px">
<defs>
<pattern id="back" width="200" height="278" patternUnits="userSpaceOnUse" >
<image width="200" href="https://svg-cdn.github.io/cm-back-red.svg" />
</pattern>
<mask id="mask1">
<rect width="100%" height="100%" fill="white"/>
<line id="LINE2" x1="0" y1="278" x2="0" y2="278"
stroke="black" stroke-width="500"
stroke-dasharray="50 50" pathLength="100" />
</mask>
<marker id="card" viewBox="-278 0 278 200" refX="0" refY="0"
markerWidth="278" markerHeight="200"
markerUnits="userSpaceOnUse" orient="auto">
<image transform="rotate(90)" width="200"
href="https://svg-cdn.github.io/cm-hearts-king.svg" />
</marker>
</defs>
<g mask="url(#mask1)">
<rect class="back" width="200" height="278" fill="url(#back)" />
<line id="LINE1" x1="0" y1="278" x2="0" y2="0" stroke="none" marker-end="url(#card)" />
</g>
</svg>

Click only through holes in svg mask

I have svg mask which determines holes in rectangular. Behind svg mask I have some clickable elements and I would like to pass events to them, but only through holes. I've experimented with pointer-events values, but I can only make either whole mask to pass events or whole mask to capture them. For one hole it can be simply done using clip-path, just determining outer part of the hole, but several holes make things more difficult. Is there any possibility to avoid using clip-path? I also tried pointer-events: visiblePainted and pointer-events: painted, but had no success.
.background {
width: 400px;
height: 400px;
background: red;
cursor: pointer;
}
.svg {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
<button class="background">
</button>
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg" class="svg">
<defs>
<mask id="mask">
<rect
x="0"
y="0"
width="400"
height="400"
fill="white"
/>
<rect
x="20"
y="20"
width="40"
height="40"
fill="black"
/>
<rect
x="290"
y="290"
width="40"
height="40"
fill="black"
/>
</mask>
</defs>
<rect
x="0"
y="0"
width="400"
height="400"
fill="black"
opacity="0.5"
mask="url(#mask)"
pointer-events="auto"
/>
</svg>
There are several aspects to this problem. First, you are right the behavior of masks and clip-paths is different in relation to hit-testing.
A clip path is a geometric boundary, and a given point is clearly either inside or outside that boundary; thus, pointer events must be captured normally over the rendered areas of a clipped element, but must not be captured over the clipped areas... By contrast, a mask is not a binary transition, but a pixel operation, and different behavior for fully transparent and almost-but-not-fully-transparent may be confusingly arbitrary; as a consequence, for elements with a mask applied, pointer events must still be captured even in areas where the mask goes to zero opacity.
Second, a clip-path is a geometric shape, but just like all paths, it might contain holes. Instead of three <rect>s, you can use one <path> with three subpaths, as long as the clip-rule makes sure the subpaths inside get cut out of the surrounding shape.
Third, if the pointer-events property is applied to an <svg> element in a HTML context, its behavior becomes...strange. Any other value than pointer-events: none on the <svg> element lead to the whole bounding box receiving events - a behavior proposed for HTML elements, but currently not part of any spec.
The solution here is to set pointer-events: none on the <svg> element, and then to reverse that with pointer-events: painted on the child <rect> element.
button, svg {
position:absolute;
width:400px;
height:400px
}
button {
background: #0000ff;
cursor: pointer;
}
button:hover {
background: #008800;
}
svg {
pointer-events: none;
}
.over {
fill: #000;
clip-path: url(#clip);
pointer-events: painted;
}
<button></button>
<svg xmlns="http://www.w3.org/2000/svg" height="400" width="400">
<defs>
<clipPath id="clip" clip-rule="evenodd">
<path d="M 20 20 h 360 v 360 h -360 z
M 40 40 v 40 h 40 v -40 z
M 200 290 v 40 h 40 v -40 z" />
</clipPath>
</defs>
<rect y="0" x="0" height="400" width="400" class="over" />
</svg>
Clip masks are useful for cropping parts out of complicated objects, but if you're just working with blocks of solid colour then maybe it would be just as easy to create shapes that already have holes in them.
I've added an example below. Does this help?
<svg width="400" heoght="200" viewBox="0 0 400 200">
<text x="100" y="100" text-anchor="middle"
alignment-baseline="middle" onclick="alert('Hello!')"
style="cursor:pointer">Click me</text>
<text x="300" y="100" text-anchor="middle"
alignment-baseline="middle" onclick="alert('Hello?')"
style="cursor:pointer">Not me</text>
<path d="M20 20 180 20 180 180 20 180ZM60 60 60 140 140
140 140 60Z" fill="#3a6" fill-opacity="0.7"
fill-rule="nonzero"/>
<path d="M220 20 380 20 380 180 220 180Z" fill="#f20"
fill-opacity="0.7"/>
</svg>

Change opacity of an area based on mouse position

Is it possible to change the opacity of background but only underneath the cursor area (for example a white small circle)? I am thinking of it a bit like a basic heatmap but the heat doesn't stay - it just follows the cursor.
At the moment I have the following
HTML:
html {
background-color: #000;
width: 100%;
height: 100%;
}
JS:
$(document).mousemove(function(event){
var i = event.pageX.toPrecision(1) / 1000;
$("html").css('opacity', i)
});
Sorry this is probably a very basic starting point. Would I need to use canvas?
You can do that using svg
What i did :-
I placed two same images with same co ordinates,height and width and gave a circular clip-path to the one on top (which has full opacity) when mouse moves the position of the circle also changes
$('svg').on('mousemove',function(e){
$('.a').attr('cx',e.pageX).attr('cy',e.pageY)
})
.one{
opacity:.5;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<svg width="500" height="500">
<clippath id="clip" >
<circle cx="50" cy="50" r="50" class="a"/>
</clippath>
<image xlink:href="https://images.unsplash.com/photo-1474575981580-1ec7944df3b2?dpr=1&auto=format&fit=crop&w=1500&h=934&q=80&cs=tinysrgb&crop=&bg=" width="500" height="500" x="0" y="0" class="one"/>
<image xlink:href="https://images.unsplash.com/photo-1474575981580-1ec7944df3b2?dpr=1&auto=format&fit=crop&w=1500&h=934&q=80&cs=tinysrgb&crop=&bg=" width="500" height="500" x="0" y="0" clip-path="url(#clip)"/>
</svg>

Fill complete image in svg container

I am new on stackoverflow.
I face a problem in svg code. I want to draw a container with a background image but when I set an image it breaks into 4 parts and gives a white space in mid of container.
This is my SVG code:
<svg id="do-as-cards" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0,0,320,340" preserveAspectRatio="xMidYMin">
<defs>
<pattern id="imgDo" preserveAspectRatio="true" patternUnits="userSpaceOnUse" y="0" x="0" width="240" height="120" >
<image xlink:href="http://s22.postimg.org/ekd89tb8x/image.png" x="0" y="0" width="407px" height="220px" />
</pattern>
<pattern id="imgAs" preserveAspectRatio="true" patternUnits="userSpaceOnUse" y="0" x="0" width="240" height="120" >
<image xlink:href="http://s22.postimg.org/72zfguwc1/image.png" x="0" y="0" width="407px" height="220px" />
</pattern>
</defs>
<g transform="translate(160,86)">
<g id="doCard" class="animatedCard" transform="matrix(1 0 0 1 0 0)" onclick="spin()">
<path class="cardOutline" d="m -150,-60 c 0,-10 10,-20 20,-20 l260,0 c 10,0 20,10 20,20 l 0,120 c 0,10 -10,20 -20,20 l -260,0 c -10,0 -20,-10 -20,-20 l 0,-120 z" />
<foreignObject id="do" class="cardFace" x="-120" y="-60" width="240" height="120"></foreignObject>
</g>
</g>
<g transform="translate(160,253)">
<g id="asCard" class="animatedCard" transform="matrix(1 0 0 1 0 0)" onclick="spin()">
<path class="cardOutline" id="as_path" d="m -150,-60 c 0,-10 10,-20 20,-20 l260,0 c 10,0 20,10 20,20 l 0,120 c 0,10 -10,20 -20,20 l -260,0 c -10,0 -20,-10 -20,-20 l 0,-120 z"/>
<foreignObject id="as" class="cardFace" x="-120" y="-60" width="240" height="120"></foreignObject>
</g>
</g>
</svg>
You can see this code in running stage by using this url
I have already tried the following:
How to set a SVG rect element's background image?
Fill SVG path element with a background-image
Using a <pattern> may not be the best way to do what you want. It can be done though.
If you are using a pattern, stick to the default patternUnits (objectBoundingBox), and set width and height to 1. Then set the width and height of your image to the max width or height of the region you are trying to fill. In the case of your example shapes, that appears to be 300. Then adjust the x and y of the <image> so it is centred in your shape.
<pattern id="imgDo" y="0" x="0" width="1" height="1" >
<image xlink:href="http://s22.postimg.org/ekd89tb8x/image.png" x="0" y="-75" width="300" height="300" />
</pattern>
Demo: http://jsfiddle.net/TRLa7/1/
Personally, I would use a <clipPath> for this situation. Use your path shape as the clipPath for the image. You will need to add an additional copy of the <path> to apply your stroke effects etc. You can define your card(s) in the <defs> section and then use a <use> to instantiate each card.
Demo: http://jsfiddle.net/TRLa7/2/

How to add image into center of svg circle?

I am trying to add an image into the center of a SVG circle.
I tried with patterns
<pattern id="image_birds" x="0" y="0" patternUnits="userSpaceOnUse" height="100" width="100">
<image x="0" y="0" xlink:href="birds.png" height="50" width="50"></image>
</pattern>
But it does not center the image. I am working with Javascript.
Clipping should do what you are looking for: https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Clipping_and_masking
Something like:
<clipPath id="cut-off-bottom">
<circle cx="100" cy="100" r="50" />
</clipPath>
<image x="25" y="25" xlink:href="http://placehold.it/150.png" height="150" width="150" clip-path="url(#cut-off-bottom)" ></image>
You can see the result of this example here: http://jsbin.com/EKUTUco/1/edit?html,output
Up to you to center the images in javascript according to their sizes, via x and y attributes.
Ok I found the answer. What I did is adding a filter to my svg:
<filter id = "i1" x = "0%" y = "0%" width = "100%" height = "100%">
<feImage xlink:href = "birds.png"/>
</filter>
and in the circle add attribute:
circle.setAttribute('filter','url(#i1)');

Categories