2.8
Animated sunscope with day cycle
This commit is contained in:
@@ -191,6 +191,42 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Wraps the dial SVG + the precip FX canvas so the canvas can overlay the
|
||||||
|
lens. Sizes itself to the SVG (220px, or 250px on mobile). */
|
||||||
|
.scope-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animated rain/snow/lightning layer, clipped to the lens circle and sitting
|
||||||
|
on top of the glass. Inset to 95% to match the lens radius (r=95 of 100). */
|
||||||
|
.scope-fx {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 95%;
|
||||||
|
height: 95%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cloud sprite strips drift horizontally inside the lens; sprites are
|
||||||
|
duplicated +200 user-units so the wrap is seamless. */
|
||||||
|
@keyframes scopeCloudDrift {
|
||||||
|
from { transform: translateX(0); }
|
||||||
|
to { transform: translateX(var(--drift, -200px)); }
|
||||||
|
}
|
||||||
|
.scope-cloud-layer {
|
||||||
|
animation: scopeCloudDrift linear infinite;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.scope-cloud-layer { animation: none; }
|
||||||
|
}
|
||||||
|
|
||||||
.scope-col {
|
.scope-col {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -198,6 +234,52 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Time-lapse toggle beneath the dial. Brass pill matching the scope bezel. */
|
||||||
|
.scope-play {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border: 1px solid rgba(200, 146, 42, 0.5);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(40, 28, 10, 0.55);
|
||||||
|
color: #e8d8c0;
|
||||||
|
font-family: 'Manrope', sans-serif;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.scope-play:hover {
|
||||||
|
background: rgba(200, 146, 42, 0.18);
|
||||||
|
border-color: rgba(200, 146, 42, 0.85);
|
||||||
|
color: #f5edd6;
|
||||||
|
}
|
||||||
|
.scope-play.is-playing {
|
||||||
|
padding: 4px 14px;
|
||||||
|
background: #241708; /* solid dark so the clock pops */
|
||||||
|
border-color: rgba(200, 146, 42, 0.9);
|
||||||
|
color: #ffd98a;
|
||||||
|
}
|
||||||
|
.scope-play.is-playing:hover {
|
||||||
|
background: #2e1d0a;
|
||||||
|
color: #ffe6b0;
|
||||||
|
}
|
||||||
|
.scope-play-icon { font-size: 9px; line-height: 1; }
|
||||||
|
/* While playing, the simulated clock is the main readout — big, bright and
|
||||||
|
monospaced so the digits don't jitter as they tick. */
|
||||||
|
.scope-play.is-playing .scope-play-label {
|
||||||
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
min-width: 52px;
|
||||||
|
text-align: center;
|
||||||
|
color: #ffd98a;
|
||||||
|
}
|
||||||
|
|
||||||
/* The environment selector sits just below the dial, centred under it.
|
/* The environment selector sits just below the dial, centred under it.
|
||||||
Kept out of .scope-col so the dial alone defines that column's height
|
Kept out of .scope-col so the dial alone defines that column's height
|
||||||
and stays vertically centred against the side columns. */
|
and stays vertically centred against the side columns. */
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
+92
-6
@@ -39,6 +39,43 @@ import { exportDayXls } from './export.js';
|
|||||||
|
|
||||||
const html = htm.bind(h);
|
const html = htm.bind(h);
|
||||||
|
|
||||||
|
// ── Scope time-lapse playback ───────────────────────────────────────────
|
||||||
|
// Drives the big scope through the next 24h so you can watch the sky/ground
|
||||||
|
// gradients and weather evolve. Speed: 15 simulated minutes every 5 real
|
||||||
|
// seconds (≈ a full day in 8 minutes). UI advances smoothly every 200ms.
|
||||||
|
const PLAYBACK_SIM_MS_PER_REAL_MS = 180; // 15 min / 5 s = 180×
|
||||||
|
const PLAYBACK_STEP_MS = 200; // UI update cadence
|
||||||
|
const PLAYBACK_WINDOW_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
// Interpolates the hourly rows to an arbitrary instant (ms). Returns a row
|
||||||
|
// with smoothly blended elevation / cloud / precip so the scope animates
|
||||||
|
// continuously rather than snapping hour to hour.
|
||||||
|
function interpolateRowAt(rows, t) {
|
||||||
|
if (!rows || rows.length === 0) return null;
|
||||||
|
if (t <= +rows[0].dt) return rows[0];
|
||||||
|
if (t >= +rows[rows.length - 1].dt) return rows[rows.length - 1];
|
||||||
|
let lo = rows[0], hi = rows[1];
|
||||||
|
for (let i = 1; i < rows.length; i++) {
|
||||||
|
if (+rows[i].dt >= t) { lo = rows[i - 1]; hi = rows[i]; break; }
|
||||||
|
}
|
||||||
|
const f = (t - +lo.dt) / (+hi.dt - +lo.dt);
|
||||||
|
const L = (a, b) => (a == null || b == null) ? (a ?? b) : a + (b - a) * f;
|
||||||
|
return {
|
||||||
|
...lo,
|
||||||
|
dt: new Date(t),
|
||||||
|
elev: L(lo.elev, hi.elev),
|
||||||
|
glob: L(lo.glob, hi.glob),
|
||||||
|
utciAdj: L(lo.utciAdj, hi.utciAdj),
|
||||||
|
cc: L(lo.cc, hi.cc),
|
||||||
|
ccLow: L(lo.ccLow, hi.ccLow),
|
||||||
|
ccMid: L(lo.ccMid, hi.ccMid),
|
||||||
|
ccHigh: L(lo.ccHigh, hi.ccHigh),
|
||||||
|
precip: L(lo.precip, hi.precip),
|
||||||
|
snow: L(lo.snow, hi.snow),
|
||||||
|
visKm: L(lo.visKm, hi.visKm),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function UTCIForecast() {
|
export function UTCIForecast() {
|
||||||
|
|
||||||
// ── STATE + EFFECTS ───────────────────────────────────────────────────
|
// ── STATE + EFFECTS ───────────────────────────────────────────────────
|
||||||
@@ -120,6 +157,25 @@ export function UTCIForecast() {
|
|||||||
};
|
};
|
||||||
}, [searchOpen, setSearchQuery]);
|
}, [searchOpen, setSearchQuery]);
|
||||||
|
|
||||||
|
// ─── SCOPE TIME-LAPSE ──────────────────────────────────────────────────
|
||||||
|
// `playing` toggles the time-lapse; `simMs` is the simulated instant shown.
|
||||||
|
const [playing, setPlaying] = useState(false);
|
||||||
|
const [simMs, setSimMs] = useState(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!playing) return;
|
||||||
|
const start = now.getTime();
|
||||||
|
const end = start + PLAYBACK_WINDOW_MS;
|
||||||
|
setSimMs(prev => (prev == null || prev < start || prev > end) ? start : prev);
|
||||||
|
const id = setInterval(() => {
|
||||||
|
setSimMs(prev => {
|
||||||
|
let next = (prev == null ? start : prev) + PLAYBACK_STEP_MS * PLAYBACK_SIM_MS_PER_REAL_MS;
|
||||||
|
if (next > end) next = start; // loop back to "now"
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, PLAYBACK_STEP_MS);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [playing]);
|
||||||
|
|
||||||
// Columns that mark the start of a logical group - used to draw a faint
|
// Columns that mark the start of a logical group - used to draw a faint
|
||||||
// vertical border separating groups in the forecast table.
|
// vertical border separating groups in the forecast table.
|
||||||
const GROUP_ORDER = {
|
const GROUP_ORDER = {
|
||||||
@@ -205,6 +261,21 @@ export function UTCIForecast() {
|
|||||||
// ─── 3b. PANEL COMPUTATIONS ──────────────────────────────────────────
|
// ─── 3b. PANEL COMPUTATIONS ──────────────────────────────────────────
|
||||||
// Derived from currentRow and the active day - no side effects.
|
// Derived from currentRow and the active day - no side effects.
|
||||||
const whyFeelsLike = computeWhyFeelsLike(currentRow ?? null, UTCI_ENVIRONMENTS[utciEnv]);
|
const whyFeelsLike = computeWhyFeelsLike(currentRow ?? null, UTCI_ENVIRONMENTS[utciEnv]);
|
||||||
|
|
||||||
|
// During time-lapse, the scope reads an interpolated row at the simulated
|
||||||
|
// instant instead of the live "now" row. Everything else stays on real now.
|
||||||
|
const simRow = (playing && simMs != null) ? interpolateRowAt(hourlyRows, simMs) : null;
|
||||||
|
const scopeRow = simRow || currentRow;
|
||||||
|
const scopeElev = simRow ? simRow.elev : (liveElev ?? currentRow?.elev ?? 0);
|
||||||
|
const scopeDt = simRow ? simRow.dt : now;
|
||||||
|
const scopeCat = simRow ? utciCategory(simRow.utciAdj) : currentCat;
|
||||||
|
// Synthesize a storm overlay (lightning) when the simulated hour is wet;
|
||||||
|
// outside playback keep the real active event.
|
||||||
|
const scopeEvent = simRow ? ((simRow.precip ?? 0) >= 4 ? { id: 'storm' } : null) : lensEvent;
|
||||||
|
// Local clock label for the playback button, e.g. "14:30".
|
||||||
|
const simClock = (playing && scopeDt)
|
||||||
|
? new Date(scopeDt.getTime() + utcOffsetMs).toISOString().slice(11, 16)
|
||||||
|
: null;
|
||||||
const staleThreshMs = isPro ? 15 * 60 * 1000 : 30 * 60 * 1000;
|
const staleThreshMs = isPro ? 15 * 60 * 1000 : 30 * 60 * 1000;
|
||||||
const isStale = fetchedAt ? (now - fetchedAt) > staleThreshMs : false;
|
const isStale = fetchedAt ? (now - fetchedAt) > staleThreshMs : false;
|
||||||
const glanceSummary = computeGlanceSummary(
|
const glanceSummary = computeGlanceSummary(
|
||||||
@@ -440,15 +511,30 @@ export function UTCIForecast() {
|
|||||||
|
|
||||||
<div class="scope-col">
|
<div class="scope-col">
|
||||||
<${ScopeReticle}
|
<${ScopeReticle}
|
||||||
value=${currentRow?.utciAdj ?? null}
|
value=${scopeRow?.utciAdj ?? null}
|
||||||
cat=${currentCat}
|
cat=${scopeCat}
|
||||||
loading=${loading}
|
loading=${loading}
|
||||||
elev=${liveElev ?? currentRow?.elev ?? 0}
|
elev=${scopeElev}
|
||||||
dt=${now}
|
dt=${scopeDt}
|
||||||
glob=${currentRow?.glob ?? 0}
|
glob=${scopeRow?.glob ?? 0}
|
||||||
activeEvent=${lensEvent}
|
activeEvent=${scopeEvent}
|
||||||
utciEnvShort=${UTCI_ENVIRONMENTS[utciEnv]?.shortLabel ?? null}
|
utciEnvShort=${UTCI_ENVIRONMENTS[utciEnv]?.shortLabel ?? null}
|
||||||
|
cc=${scopeRow?.cc ?? null}
|
||||||
|
ccLow=${scopeRow?.ccLow ?? null}
|
||||||
|
ccMid=${scopeRow?.ccMid ?? null}
|
||||||
|
ccHigh=${scopeRow?.ccHigh ?? null}
|
||||||
|
precip=${scopeRow?.precip ?? 0}
|
||||||
|
snow=${scopeRow?.snow ?? 0}
|
||||||
|
visKm=${scopeRow?.visKm ?? null}
|
||||||
/>
|
/>
|
||||||
|
${hourlyRows.length > 0 && html`
|
||||||
|
<button type="button"
|
||||||
|
class=${`scope-play ${playing ? 'is-playing' : ''}`}
|
||||||
|
onClick=${() => setPlaying(p => !p)}
|
||||||
|
title=${playing ? 'Stop time-lapse' : 'Play a 24-hour time-lapse on the scope'}>
|
||||||
|
<span class="scope-play-icon">${playing ? '◼' : '▶'}</span>
|
||||||
|
<span class="scope-play-label">${playing ? simClock : 'Day cycle'}</span>
|
||||||
|
</button>`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|||||||
+151
-4
File diff suppressed because one or more lines are too long
@@ -82,10 +82,8 @@ export function getLensOverlaySVG(event, cx, cy, lensR) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (id === 'wet') {
|
if (id === 'wet') {
|
||||||
const sl = [-55,-30,-5,20,45,65].map(function(x){
|
// Rain is now drawn continuously on the scope FX canvas; no static overlay.
|
||||||
return '<line x1="' + (cx+x) + '" y1="' + (cy-lensR+10) + '" x2="' + (cx+x-15) + '" y2="' + (cy+lensR-10) + '" stroke="rgba(140,180,220,0.6)" stroke-width="1" stroke-linecap="round" />';
|
return null;
|
||||||
}).join('');
|
|
||||||
return '<g clip-path="url(#scope-lens-clip)" opacity="0.45">' + sl + '</g>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === 'wind') {
|
if (id === 'wind') {
|
||||||
@@ -96,12 +94,10 @@ export function getLensOverlaySVG(event, cx, cy, lensR) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (id === 'storm') {
|
if (id === 'storm') {
|
||||||
// Driving rain streaks plus a forked lightning bolt.
|
// Driving rain + lightning flashes are drawn on the FX canvas; keep just
|
||||||
const sl = [-55,-30,-5,20,45,65].map(function(x){
|
// the static forked bolt as the event signifier.
|
||||||
return '<line x1="' + (cx+x) + '" y1="' + (cy-lensR+10) + '" x2="' + (cx+x-22) + '" y2="' + (cy+lensR-10) + '" stroke="rgba(140,180,220,0.55)" stroke-width="1" stroke-linecap="round" />';
|
|
||||||
}).join('');
|
|
||||||
const bolt = '<path d="M ' + (cx+6) + ' ' + (cy-lensR+18) + ' L ' + (cx-14) + ' ' + (cy+8) + ' L ' + (cx-2) + ' ' + (cy+8) + ' L ' + (cx-18) + ' ' + (cy+lensR-14) + ' L ' + (cx+18) + ' ' + (cy-2) + ' L ' + (cx+4) + ' ' + (cy-2) + ' Z" fill="rgba(255,224,120,0.7)" stroke="rgba(255,200,60,0.8)" stroke-width="1" stroke-linejoin="round" />';
|
const bolt = '<path d="M ' + (cx+6) + ' ' + (cy-lensR+18) + ' L ' + (cx-14) + ' ' + (cy+8) + ' L ' + (cx-2) + ' ' + (cy+8) + ' L ' + (cx-18) + ' ' + (cy+lensR-14) + ' L ' + (cx+18) + ' ' + (cy-2) + ' L ' + (cx+4) + ' ' + (cy-2) + ' Z" fill="rgba(255,224,120,0.7)" stroke="rgba(255,200,60,0.8)" stroke-width="1" stroke-linejoin="round" />';
|
||||||
return '<g clip-path="url(#scope-lens-clip)" opacity="0.5">' + sl + bolt + '</g>';
|
return '<g clip-path="url(#scope-lens-clip)" opacity="0.5">' + bolt + '</g>';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === 'frost') {
|
if (id === 'frost') {
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>scope cell</title>
|
||||||
|
<link rel="stylesheet" href="./assets/css/header.css" />
|
||||||
|
<style>
|
||||||
|
html,body { margin:0; height:100%; background: transparent; overflow: hidden; }
|
||||||
|
#cell { display:flex; align-items:center; justify-content:center; height:100%; }
|
||||||
|
.scope-ring { width: 200px; height: 200px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="cell"></div>
|
||||||
|
<script type="module">
|
||||||
|
import { h, render } from './assets/vendor/preact.js';
|
||||||
|
import { ScopeReticle } from './assets/js/components.js';
|
||||||
|
|
||||||
|
const q = new URLSearchParams(location.search);
|
||||||
|
const n = (k, d) => { const v = q.get(k); return v == null ? d : Number(v); };
|
||||||
|
|
||||||
|
// hour drives the rising/setting palette (UTC hour < 12 => sunrise tints).
|
||||||
|
const dt = new Date(Date.UTC(2026, 5, 6, n('hour', 12), 0, 0));
|
||||||
|
const cat = { label: 'Comfortable', bg: '#90d090', fg: '#157a15' };
|
||||||
|
const storm = q.get('storm') === '1';
|
||||||
|
|
||||||
|
render(h(ScopeReticle, {
|
||||||
|
value: 12.4, cat, loading: false,
|
||||||
|
elev: n('elev', 30), dt, glob: n('glob', 400),
|
||||||
|
activeEvent: storm ? { id: 'storm' } : null,
|
||||||
|
cc: n('cc', 0), ccLow: n('ccLow', 0), ccMid: n('ccMid', 0), ccHigh: n('ccHigh', 0),
|
||||||
|
precip: n('precip', 0), snow: n('snow', 0), visKm: n('vis', 30),
|
||||||
|
}), document.getElementById('cell'));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>SunScope — Scope Lab (dev)</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; background: #1a140c; font-family: Manrope, system-ui, sans-serif; color: #f5edd6; }
|
||||||
|
h1 { text-align: center; font-weight: 700; font-size: 18px; padding: 16px 0 4px; opacity: 0.8; }
|
||||||
|
p.note { text-align:center; opacity:0.45; font-size:12px; margin:0 0 12px; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(4, 220px); gap: 18px 18px; justify-content: center; padding: 16px 16px 40px; }
|
||||||
|
.cell { display: flex; flex-direction: column; align-items: center; gap: 4px; }
|
||||||
|
.label { font-size: 12px; font-weight: 700; letter-spacing: 0.4px; opacity: 0.9; }
|
||||||
|
iframe { width: 220px; height: 220px; border: 0; background: transparent; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>SunScope — Scope Lab</h1>
|
||||||
|
<p class="note">dev preview · not shipped · each scope is a real ScopeReticle instance</p>
|
||||||
|
<div id="lab" class="grid"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const clear = 'cc=0&ccLow=0&ccMid=0&ccHigh=0&precip=0&snow=0&vis=30';
|
||||||
|
const cloudy = 'cc=75&ccLow=60&ccMid=50&ccHigh=30&precip=0&snow=0&vis=20';
|
||||||
|
const rain = 'cc=95&ccLow=90&ccMid=70&ccHigh=40&precip=5&snow=0&vis=6';
|
||||||
|
const snowwx = 'cc=95&ccLow=90&ccMid=70&ccHigh=30&precip=0&snow=1.2&vis=4';
|
||||||
|
const storm = 'cc=100&ccLow=95&ccMid=80&ccHigh=50&precip=9&snow=0&vis=3&storm=1';
|
||||||
|
|
||||||
|
// hour: 7 = sunrise palette, 19 = sunset palette, 12 = noon, 23 = night
|
||||||
|
const scenes = [
|
||||||
|
['Day · clear', 'elev=45&hour=12&glob=800&' + clear],
|
||||||
|
['Day · cloudy', 'elev=45&hour=12&glob=500&' + cloudy],
|
||||||
|
['Day · rain', 'elev=45&hour=12&glob=250&' + rain],
|
||||||
|
['Day · snow', 'elev=30&hour=12&glob=200&' + snowwx],
|
||||||
|
|
||||||
|
['Sunrise · clear', 'elev=2&hour=7&glob=120&' + clear],
|
||||||
|
['Sunrise · cloudy', 'elev=2&hour=7&glob=90&' + cloudy],
|
||||||
|
['Sunrise · rain', 'elev=2&hour=7&glob=60&' + rain],
|
||||||
|
['Sunrise · snow', 'elev=2&hour=7&glob=70&' + snowwx],
|
||||||
|
|
||||||
|
['Sunset · clear', 'elev=2&hour=19&glob=120&' + clear],
|
||||||
|
['Sunset · cloudy', 'elev=2&hour=19&glob=90&' + cloudy],
|
||||||
|
['Sunset · rain', 'elev=2&hour=19&glob=60&' + rain],
|
||||||
|
['Sunset · storm', 'elev=1&hour=19&glob=40&' + storm],
|
||||||
|
|
||||||
|
['Night · clear', 'elev=-15&hour=23&glob=0&' + clear],
|
||||||
|
['Night · cloudy', 'elev=-15&hour=23&glob=0&' + cloudy],
|
||||||
|
['Night · rain', 'elev=-12&hour=23&glob=0&' + rain],
|
||||||
|
['Night · snow', 'elev=-12&hour=23&glob=0&' + snowwx],
|
||||||
|
];
|
||||||
|
|
||||||
|
const lab = document.getElementById('lab');
|
||||||
|
for (const [label, qs] of scenes) {
|
||||||
|
const cell = document.createElement('div');
|
||||||
|
cell.className = 'cell';
|
||||||
|
const lab2 = document.createElement('span');
|
||||||
|
lab2.className = 'label';
|
||||||
|
lab2.textContent = label;
|
||||||
|
const ifr = document.createElement('iframe');
|
||||||
|
ifr.src = './scope-cell.html?' + qs;
|
||||||
|
ifr.loading = 'eager';
|
||||||
|
cell.append(lab2, ifr);
|
||||||
|
lab.append(cell);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user