фото/видео пациентов: SVG-портреты (фото в карточке + говорящее видео с мимикой)
This commit is contained in:
parent
38bee301c3
commit
a54c5ea11e
36
app.js
36
app.js
@ -718,7 +718,7 @@ function renderCards(filter="all"){
|
||||
el.className=`case-card t-${c.theme}`;
|
||||
el.innerHTML=`
|
||||
<div class="case-head">
|
||||
<div class="case-avatar">${c.patient.avatar}</div>
|
||||
<div class="case-avatar">${portrait(c.patient)}</div>
|
||||
<div>
|
||||
<h3>${c.title}</h3>
|
||||
<div class="case-sub">${c.patient.name}, ${c.patient.age.split(",")[0]}</div>
|
||||
@ -868,10 +868,38 @@ function ring(pct, size, stroke, color, track, num){
|
||||
</svg><div class="timer-num">${num}</div></div>`;
|
||||
}
|
||||
|
||||
function portrait(p){
|
||||
const ageM=(p.age||"").match(/(\d+)/); const age=ageM?+ageM[1]:40;
|
||||
const male=/мужчин|муж|мальчик/i.test(p.age||"")||/муж/i.test(p.name||"");
|
||||
const old=age>=55;
|
||||
const skin="#e8b894", skinSh="#cf9c75";
|
||||
const hair=old?"#cfc8bf":(male?"#2e2018":"#5a3a22");
|
||||
const cloth=male?"#33414f":"#7b2d3b";
|
||||
const hairEl = male
|
||||
? `<path d="M56 80 Q58 38 100 36 Q142 38 144 80 Q138 56 100 54 Q62 56 56 80 Z" fill="${hair}"/>${old?`<path d="M72 58 Q100 48 128 58 Q116 70 100 68 Q84 70 72 58 Z" fill="${skin}" opacity=".55"/>`:""}`
|
||||
: `<path d="M52 124 Q46 70 100 48 Q154 70 148 124 Q150 92 100 82 Q50 92 52 124 Z" fill="${hair}"/><path d="M58 80 Q62 40 100 38 Q138 40 142 80 Q128 54 100 52 Q72 54 58 80 Z" fill="${hair}"/>`;
|
||||
return `<svg class="por" viewBox="0 0 200 220" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M28 220 Q28 162 100 158 Q172 162 172 220 Z" fill="${cloth}"/>
|
||||
<path d="M86 150 L86 170 Q100 178 114 170 L114 150 Z" fill="${skinSh}"/>
|
||||
<ellipse cx="100" cy="96" rx="44" ry="54" fill="${skin}"/>
|
||||
<ellipse cx="57" cy="100" rx="8" ry="12" fill="${skinSh}"/>
|
||||
<ellipse cx="143" cy="100" rx="8" ry="12" fill="${skinSh}"/>
|
||||
${hairEl}
|
||||
<rect x="71" y="81" width="22" height="4" rx="2" fill="${hair}"/>
|
||||
<rect x="107" y="81" width="22" height="4" rx="2" fill="${hair}"/>
|
||||
<g class="eye"><ellipse cx="83" cy="97" rx="10" ry="7" fill="#fff"/><circle cx="84" cy="97" r="4" fill="#3a2a20"/><rect class="eyelid" x="71" y="90" width="24" height="14" fill="${skin}"/></g>
|
||||
<g class="eye"><ellipse cx="117" cy="97" rx="10" ry="7" fill="#fff"/><circle cx="116" cy="97" r="4" fill="#3a2a20"/><rect class="eyelid" x="105" y="90" width="24" height="14" fill="${skin}"/></g>
|
||||
<path d="M100 102 L93 122 Q100 126 107 122 Z" fill="${skinSh}"/>
|
||||
<ellipse class="mouth" cx="100" cy="134" rx="15" ry="5" fill="#9b3a3a"/>
|
||||
<ellipse cx="72" cy="118" rx="9" ry="5" fill="#e8a08a" opacity=".5"/>
|
||||
<ellipse cx="128" cy="118" rx="9" ry="5" fill="#e8a08a" opacity=".5"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function patientBlock(c){
|
||||
return `<div class="patient">
|
||||
<div class="patient-top">
|
||||
<div class="patient-avatar" id="pAvatar">${c.patient.avatar}</div>
|
||||
<div class="patient-avatar" id="pAvatar">${portrait(c.patient)}</div>
|
||||
<div class="patient-info">
|
||||
<h2>${c.title}</h2>
|
||||
<div class="meta">${c.patient.name} · ${c.patient.age} · ${c.patient.ctx}</div>
|
||||
@ -881,7 +909,7 @@ function patientBlock(c){
|
||||
<div class="vplayer" id="vplayer">
|
||||
<div class="vp-screen">
|
||||
<div class="vp-waves"><i></i><i></i><i></i></div>
|
||||
<div class="vp-avatar">${c.patient.avatar}</div>
|
||||
<div class="vp-portrait" id="pPortrait">${portrait(c.patient)}</div>
|
||||
<span class="vp-live">● ВИДЕО · LIVE</span>
|
||||
<span class="vp-photo">📷 Фото пациента</span>
|
||||
</div>
|
||||
@ -906,7 +934,7 @@ function vitalsHTML(v){
|
||||
}
|
||||
function bindSpeak(c, root){
|
||||
const btn=root.querySelector("#speakBtn"),
|
||||
av=root.querySelector(".vp-avatar"),
|
||||
av=root.querySelector(".vp-portrait"),
|
||||
player=root.querySelector("#vplayer"),
|
||||
subs=root.querySelector("#vpSubs");
|
||||
if(!btn) return;
|
||||
|
||||
19
style.css
19
style.css
@ -150,7 +150,8 @@ h1,h2,h3{letter-spacing:-.4px}
|
||||
.case-card.t-odn::before{background:var(--teal)}
|
||||
.case-card.t-shock::before{background:var(--rust)}
|
||||
.case-head{display:flex;align-items:center;gap:15px;margin-bottom:12px}
|
||||
.case-avatar{width:56px;height:56px;border-radius:50%;display:grid;place-items:center;font-size:30px;background:var(--paper);flex-shrink:0}
|
||||
.case-avatar{width:64px;height:64px;border-radius:50%;display:grid;place-items:center;background:var(--paper);flex-shrink:0;overflow:hidden}
|
||||
.case-avatar .por{width:118%;height:auto;display:block}
|
||||
.case-head h3{font-size:19px;font-weight:800;margin-bottom:2px}
|
||||
.case-head .case-sub{font-size:13.5px;color:var(--gray-500)}
|
||||
.badge{display:inline-block;padding:4px 10px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.3px;text-transform:uppercase}
|
||||
@ -263,7 +264,8 @@ h1,h2,h3{letter-spacing:-.4px}
|
||||
/* шаг пациента */
|
||||
.patient{background:linear-gradient(135deg,var(--navy) 0%,#16203a 100%);color:#fff;border-radius:var(--radius-lg);padding:28px;margin-bottom:18px;position:relative;overflow:hidden}
|
||||
.patient-top{display:flex;align-items:center;gap:18px;margin-bottom:18px}
|
||||
.patient-avatar{width:76px;height:76px;border-radius:50%;background:rgba(255,255,255,.1);display:grid;place-items:center;font-size:40px;flex-shrink:0;position:relative}
|
||||
.patient-avatar{width:78px;height:78px;border-radius:50%;background:rgba(255,255,255,.1);display:grid;place-items:center;flex-shrink:0;position:relative;overflow:hidden}
|
||||
.patient-avatar .por{width:120%;height:auto;display:block}
|
||||
.patient-avatar.is-speaking::after{content:"";position:absolute;inset:-6px;border-radius:50%;border:2px solid var(--gold);animation:pulse 1.1s ease-out infinite}
|
||||
.patient-avatar.is-speaking::before{content:"";position:absolute;inset:-12px;border-radius:50%;border:2px solid rgba(176,141,87,.4);animation:pulse 1.1s ease-out .3s infinite}
|
||||
@keyframes pulse{0%{transform:scale(1);opacity:.9}100%{transform:scale(1.4);opacity:0}}
|
||||
@ -289,9 +291,16 @@ h1,h2,h3{letter-spacing:-.4px}
|
||||
.vplayer.is-speaking .vp-waves i:nth-child(2){animation-delay:.55s}
|
||||
.vplayer.is-speaking .vp-waves i:nth-child(3){animation-delay:1.1s}
|
||||
@keyframes vpwave{0%{transform:scale(.55);opacity:.7}100%{transform:scale(2.3);opacity:0}}
|
||||
.vp-avatar{font-size:104px;line-height:1;position:relative;z-index:2;filter:drop-shadow(0 10px 18px rgba(0,0,0,.45))}
|
||||
.vplayer.is-speaking .vp-avatar{animation:vptalk .45s ease-in-out infinite alternate}
|
||||
@keyframes vptalk{0%{transform:scale(1) translateY(0)}100%{transform:scale(1.05) translateY(-4px)}}
|
||||
.vp-portrait{width:58%;max-width:240px;position:relative;z-index:2;height:auto}
|
||||
.vp-portrait .por{width:100%;height:auto;display:block;filter:drop-shadow(0 10px 18px rgba(0,0,0,.45))}
|
||||
.vplayer.is-speaking .vp-portrait{animation:vptalk .45s ease-in-out infinite alternate}
|
||||
@keyframes vptalk{0%{transform:translateY(0)}100%{transform:translateY(-4px)}}
|
||||
.por{width:100%;height:auto;display:block}
|
||||
/* мимика */
|
||||
.vplayer.is-speaking .mouth{transform-origin:100px 134px;animation:talk .34s ease-in-out infinite alternate}
|
||||
@keyframes talk{0%{transform:scaleY(.55)}100%{transform:scaleY(1.9)}}
|
||||
.eyelid{transform:translateY(-16px);animation:blink 5.5s infinite}
|
||||
@keyframes blink{0%,90%,100%{transform:translateY(-16px)}93%,96%{transform:translateY(0)}}
|
||||
.vp-live{position:absolute;top:12px;left:12px;background:rgba(0,0,0,.45);color:#fff;font-size:11px;font-weight:800;letter-spacing:.5px;padding:4px 10px;border-radius:6px;z-index:3}
|
||||
.vplayer.is-speaking .vp-live{color:#ff6b6b}
|
||||
.vp-photo{position:absolute;top:12px;right:12px;background:rgba(0,0,0,.45);color:#cfd6e0;font-size:11px;font-weight:700;padding:4px 10px;border-radius:6px;z-index:3}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user