Created
November 14, 2020 22:50
-
-
Save balloob/1f1e3baf76915adc7d89bfa2af499d92 to your computer and use it in GitHub Desktop.
Custom card for Home Assistant that shows a pull Light Card. Video at https://twitter.com/balloob/status/1327745146633510912
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Created by @jh3yy | |
Adapted for Home Assistant by @balloob | |
Original: https://twitter.com/jh3yy/status/1327686213432717313 | |
Only works on localhost because of restrictions MorphSVGPlugin3 | |
Card config for usage in Home Assistant: | |
type: "custom:pull-light-card" | |
entity: light.kitchen | |
Works with any entity that has on/off states and can be toggled. | |
*/ | |
import { | |
LitElement, | |
html, | |
svg, | |
css, | |
} from "https://unpkg.com/[email protected]/lit-element.js?module"; | |
let loaded = undefined; | |
function loadJS(url) { | |
return new Promise((resolve, reject) => { | |
const element = document.createElement("script"); | |
let attr = "src"; | |
let parent = "body"; | |
// Important success and error for the promise | |
element.onload = () => resolve(url); | |
element.onerror = () => reject(url); | |
// Inject into document to kick off loading | |
element.src = url; | |
document.body.appendChild(element); | |
}); | |
} | |
// GSAP has a bug that it can't be imported from a module. | |
async function loadGSAP() { | |
for (const url of [ | |
"https://unpkg.com/gsap@3/dist/gsap.min.js", | |
"https://assets.codepen.io/16327/MorphSVGPlugin3.min.js", | |
"https://unpkg.com/gsap@3/dist/Draggable.min.js", | |
]) { | |
await loadJS(url); | |
} | |
} | |
class PullLightCard extends LitElement { | |
static get properties() { | |
return { | |
hass: {}, // HomeAssistant; | |
}; | |
} | |
firstUpdated() { | |
super.firstUpdated(); | |
if (!loaded) { | |
loaded = loadGSAP(); | |
} | |
loaded.then(() => this.initGSAP()); | |
} | |
updated(props) { | |
super.updated(props); | |
const newState = this.hass.states[this.config.entity].state == "on"; | |
if (newState != this.curState) { | |
this.setState(newState); | |
} | |
} | |
setState() { | |
// placeholder | |
} | |
initGSAP() { | |
const { | |
gsap: { registerPlugin, set, to, timeline }, | |
MorphSVGPlugin, | |
Draggable, | |
} = window; | |
registerPlugin(MorphSVGPlugin); | |
// Used to calculate distance of "tug" | |
let startX; | |
let startY; | |
const AUDIO = { | |
CLICK: new Audio("https://assets.codepen.io/605876/click.mp3"), | |
}; | |
const STATE = { | |
ON: false, | |
}; | |
const CORD_DURATION = 0.1; | |
const CORDS = this.shadowRoot.querySelectorAll(".toggle-scene__cord"); | |
const HIT = this.shadowRoot.querySelector(".toggle-scene__hit-spot"); | |
const DUMMY = this.shadowRoot.querySelector(".toggle-scene__dummy-cord"); | |
const DUMMY_CORD = this.shadowRoot.querySelector( | |
".toggle-scene__dummy-cord line" | |
); | |
const PROXY = document.createElement("div"); | |
// set init position | |
const ENDX = DUMMY_CORD.getAttribute("x2"); | |
const ENDY = DUMMY_CORD.getAttribute("y2"); | |
const RESET = () => { | |
set(PROXY, { | |
x: ENDX, | |
y: ENDY, | |
}); | |
}; | |
RESET(); | |
this.setState = (newState) => { | |
set(this, { "--on": newState ? 1 : 0 }); | |
set(DUMMY, { display: "none" }); | |
set(CORDS[0], { display: "block" }); | |
this.curState = newState; | |
}; | |
const CORD_TL = timeline({ | |
paused: true, | |
onStart: () => { | |
this.hass.callService("homeassistant", "toggle", { | |
entity_id: this.config.entity, | |
}); | |
this.setState(!this.curState); | |
AUDIO.CLICK.play(); | |
}, | |
onComplete: () => { | |
set(DUMMY, { display: "block" }); | |
set(CORDS[0], { display: "none" }); | |
RESET(); | |
}, | |
}); | |
for (let i = 1; i < CORDS.length; i++) { | |
CORD_TL.add( | |
to(CORDS[0], { | |
morphSVG: CORDS[i], | |
duration: CORD_DURATION, | |
repeat: 1, | |
yoyo: true, | |
}) | |
); | |
} | |
Draggable.create(PROXY, { | |
trigger: HIT, | |
type: "x,y", | |
onPress: (e) => { | |
startX = e.x; | |
startY = e.y; | |
}, | |
onDrag: function () { | |
set(DUMMY_CORD, { | |
attr: { | |
x2: this.x, | |
y2: this.y, | |
}, | |
}); | |
}, | |
onRelease: function (e) { | |
const DISTX = Math.abs(e.x - startX); | |
const DISTY = Math.abs(e.y - startY); | |
const TRAVELLED = Math.sqrt(DISTX * DISTX + DISTY * DISTY); | |
to(DUMMY_CORD, { | |
attr: { x2: ENDX, y2: ENDY }, | |
duration: CORD_DURATION, | |
onComplete: () => { | |
if (TRAVELLED > 50) { | |
CORD_TL.restart(); | |
} else { | |
RESET(); | |
} | |
}, | |
}); | |
}, | |
}); | |
this.setState(this.hass.states[this.config.entity].state == "on"); | |
} | |
getCardSize() { | |
return 8; | |
} | |
setConfig(config) { | |
this.config = config; | |
} | |
render() { | |
return html` | |
<ha-card> | |
${svg` | |
<svg class="toggle-scene" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin" viewBox="0 0 197.451 481.081" style="touch-action: none;"> | |
<defs> | |
<marker id="e" orient="auto" overflow="visible" refX="0" refY="0"> | |
<path class="toggle-scene__cord-end" fill-rule="evenodd" stroke-width=".2666" d="M.98 0a1 1 0 11-2 0 1 1 0 012 0z"></path> | |
</marker> | |
<marker id="d" orient="auto" overflow="visible" refX="0" refY="0"> | |
<path class="toggle-scene__cord-end" fill-rule="evenodd" stroke-width=".2666" d="M.98 0a1 1 0 11-2 0 1 1 0 012 0z"></path> | |
</marker> | |
<marker id="c" orient="auto" overflow="visible" refX="0" refY="0"> | |
<path class="toggle-scene__cord-end" fill-rule="evenodd" stroke-width=".2666" d="M.98 0a1 1 0 11-2 0 1 1 0 012 0z"></path> | |
</marker> | |
<marker id="b" orient="auto" overflow="visible" refX="0" refY="0"> | |
<path class="toggle-scene__cord-end" fill-rule="evenodd" stroke-width=".2666" d="M.98 0a1 1 0 11-2 0 1 1 0 012 0z"></path> | |
</marker> | |
<marker id="a" orient="auto" overflow="visible" refX="0" refY="0"> | |
<path class="toggle-scene__cord-end" fill-rule="evenodd" stroke-width=".2666" d="M.98 0a1 1 0 11-2 0 1 1 0 012 0z"></path> | |
</marker> | |
<clipPath id="g" clipPathUnits="userSpaceOnUse"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="4.677" d="M-774.546 827.629s12.917-13.473 29.203-13.412c16.53.062 29.203 13.412 29.203 13.412v53.6s-8.825 16-29.203 16c-21.674 0-29.203-16-29.203-16z"></path> | |
</clipPath> | |
<clipPath id="f" clipPathUnits="userSpaceOnUse"> | |
<path d="M-868.418 945.051c-4.188 73.011 78.255 53.244 150.216 52.941 82.387-.346 98.921-19.444 98.921-47.058 0-27.615-4.788-42.55-73.823-42.55-69.036 0-171.436-30.937-175.314 36.667z"></path> | |
</clipPath> | |
</defs> | |
<g class="toggle-scene__cords"> | |
<path class="toggle-scene__cord" marker-end="url(#a)" fill="none" stroke-linecap="square" stroke-width="6" d="M123.228-28.56v150.493" transform="translate(-24.503 256.106)"></path> | |
<path class="toggle-scene__cord" marker-end="url(#a)" fill="none" stroke-linecap="square" stroke-width="6" d="M123.228-28.59s28 8.131 28 19.506-18.667 13.005-28 19.507c-9.333 6.502-28 8.131-28 19.506s28 19.507 28 19.507" transform="translate(-24.503 256.106)"></path> | |
<path class="toggle-scene__cord" marker-end="url(#a)" fill="none" stroke-linecap="square" stroke-width="6" d="M123.228-28.575s-20 16.871-20 28.468c0 11.597 13.333 18.978 20 28.468 6.667 9.489 20 16.87 20 28.467 0 11.597-20 28.468-20 28.468" transform="translate(-24.503 256.106)"></path> | |
<path class="toggle-scene__cord" marker-end="url(#a)" fill="none" stroke-linecap="square" stroke-width="6" d="M123.228-28.569s16 20.623 16 32.782c0 12.16-10.667 21.855-16 32.782-5.333 10.928-16 20.623-16 32.782 0 12.16 16 32.782 16 32.782" transform="translate(-24.503 256.106)"></path> | |
<path class="toggle-scene__cord" marker-end="url(#a)" fill="none" stroke-linecap="square" stroke-width="6" d="M123.228-28.563s-10 24.647-10 37.623c0 12.977 6.667 25.082 10 37.623 3.333 12.541 10 24.647 10 37.623 0 12.977-10 37.623-10 37.623" transform="translate(-24.503 256.106)"></path> | |
<g class="line toggle-scene__dummy-cord"> | |
<line marker-end="url(#a)" x1="98.7255" x2="98.7255" y1="240.5405" y2="380.5405"></line> | |
</g> | |
<circle class="toggle-scene__hit-spot" cx="98.7255" cy="380.5405" r="60" fill="transparent" style="touch-action: none; cursor: grab; user-select: none;"></circle> | |
</g> | |
<g class="toggle-scene__bulb bulb" transform="translate(844.069 -645.213)"> | |
<path class="bulb__cap" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.677" d="M-774.546 827.629s12.917-13.473 29.203-13.412c16.53.062 29.203 13.412 29.203 13.412v53.6s-8.825 16-29.203 16c-21.674 0-29.203-16-29.203-16z"></path> | |
<path class="bulb__cap-shine" d="M-778.379 802.873h25.512v118.409h-25.512z" clip-path="url(#g)" transform="matrix(.52452 0 0 .90177 -368.282 82.976)"></path> | |
<path class="bulb__cap" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M-774.546 827.629s12.917-13.473 29.203-13.412c16.53.062 29.203 13.412 29.203 13.412v0s-8.439 10.115-28.817 10.115c-21.673 0-29.59-10.115-29.59-10.115z"></path> | |
<path class="bulb__cap-outline" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.677" d="M-774.546 827.629s12.917-13.473 29.203-13.412c16.53.062 29.203 13.412 29.203 13.412v53.6s-8.825 16-29.203 16c-21.674 0-29.203-16-29.203-16z"></path> | |
<g class="bulb__filament" fill="none" stroke-linecap="round" stroke-width="5"> | |
<path d="M-752.914 823.875l-8.858-33.06"></path> | |
<path d="M-737.772 823.875l8.858-33.06"></path> | |
</g> | |
<path class="bulb__bulb" stroke-linecap="round" stroke-width="5" d="M-783.192 803.855c5.251 8.815 5.295 21.32 13.272 27.774 12.299 8.045 36.46 8.115 49.127 0 7.976-6.454 8.022-18.96 13.273-27.774 3.992-6.7 14.408-19.811 14.408-19.811 8.276-11.539 12.769-24.594 12.769-38.699 0-35.898-29.102-65-65-65-35.899 0-65 29.102-65 65 0 13.667 4.217 26.348 12.405 38.2 0 0 10.754 13.61 14.746 20.31z"></path> | |
<circle class="bulb__flash" cx="-745.343" cy="743.939" r="83.725" fill="none" stroke-dasharray="10,30" stroke-linecap="round" stroke-linejoin="round" stroke-width="10"></circle> | |
<path class="bulb__shine" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="12" d="M-789.19 757.501a45.897 45.897 0 013.915-36.189 45.897 45.897 0 0129.031-21.957"></path> | |
</g> | |
</svg> | |
`} | |
</ha-card> | |
`; | |
} | |
static get styles() { | |
return css` | |
* { | |
box-sizing: border-box; | |
} | |
:host { | |
--on: 0; | |
--bg: hsl( | |
calc(200 - (var(--on) * 160)), | |
calc((20 + (var(--on) * 50)) * 1%), | |
calc((20 + (var(--on) * 60)) * 1%) | |
); | |
--cord: hsl(0, 0%, calc((60 - (var(--on) * 50)) * 1%)); | |
--stroke: hsl(0, 0%, calc((60 - (var(--on) * 50)) * 1%)); | |
--shine: hsla(0, 0%, 100%, calc(0.75 - (var(--on) * 0.5))); | |
--cap: hsl(0, 0%, calc((40 + (var(--on) * 30)) * 1%)); | |
--filament: hsl( | |
45, | |
calc(var(--on) * 80%), | |
calc((25 + (var(--on) * 75)) * 1%) | |
); | |
} | |
ha-card { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
background: var(--bg); | |
min-height: 550px; | |
} | |
.toggle-scene { | |
overflow: visible !important; | |
height: 50vmin; | |
position: absolute; | |
} | |
.toggle-scene__cord { | |
stroke: var(--cord); | |
cursor: move; | |
} | |
.toggle-scene__cord:nth-of-type(1) { | |
display: none; | |
} | |
.toggle-scene__cord:nth-of-type(2), | |
.toggle-scene__cord:nth-of-type(3), | |
.toggle-scene__cord:nth-of-type(4), | |
.toggle-scene__cord:nth-of-type(5) { | |
display: none; | |
} | |
.toggle-scene__cord-end { | |
stroke: var(--cord); | |
fill: var(--cord); | |
} | |
.toggle-scene__dummy-cord { | |
stroke-width: 6; | |
stroke: var(--cord); | |
} | |
.bulb__filament { | |
stroke: var(--filament); | |
} | |
.bulb__shine { | |
stroke: var(--shine); | |
} | |
.bulb__flash { | |
stroke: #f5e0a3; | |
display: none; | |
} | |
.bulb__bulb { | |
stroke: var(--stroke); | |
fill: hsla( | |
calc(180 - (95 * var(--on))), | |
80%, | |
80%, | |
calc(0.1 + (0.4 * var(--on))) | |
); | |
} | |
.bulb__cap { | |
fill: var(--cap); | |
} | |
.bulb__cap-shine { | |
fill: var(--shine); | |
} | |
.bulb__cap-outline { | |
stroke: var(--stroke); | |
} | |
`; | |
} | |
} | |
customElements.define("pull-light-card", PullLightCard); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment