Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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>

like image 506
Danny '365CSI' Engelman Avatar asked Oct 12 '25 11:10

Danny '365CSI' Engelman


2 Answers

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>
like image 153
ccprog Avatar answered Oct 13 '25 23:10

ccprog


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).

like image 20
Heiko Theißen Avatar answered Oct 14 '25 01:10

Heiko Theißen



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!