Skip to content

Commit ba6a1be

Browse files
authored
1 parent 3e633af commit ba6a1be

File tree

1 file changed

+325
-0
lines changed

1 file changed

+325
-0
lines changed

svg-progressive-render.html

+325
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<style>
5+
* {
6+
box-sizing: border-box;
7+
}
8+
9+
body {
10+
font-family: system-ui, -apple-system, sans-serif;
11+
max-width: 800px;
12+
margin: 0 auto;
13+
padding: 1em;
14+
background: #f5f5f5;
15+
}
16+
17+
.input-group {
18+
display: flex;
19+
gap: 10px;
20+
margin-bottom: 20px;
21+
align-items: center;
22+
}
23+
24+
.input-group textarea {
25+
flex-grow: 1;
26+
height: 2.8em;
27+
padding: 8px 12px;
28+
font-family: monospace;
29+
border: 2px solid #ccc;
30+
border-radius: 4px;
31+
resize: none;
32+
}
33+
34+
.duration-input {
35+
width: 60px;
36+
height: 100%;
37+
padding: 8px;
38+
border: 2px solid #ccc;
39+
border-radius: 4px;
40+
font-size: 14px;
41+
}
42+
43+
.duration-label {
44+
font-size: 14px;
45+
color: #666;
46+
white-space: nowrap;
47+
}
48+
49+
button {
50+
padding: 0 20px;
51+
background: #0066cc;
52+
color: white;
53+
border: none;
54+
border-radius: 4px;
55+
cursor: pointer;
56+
font-size: 14px;
57+
font-weight: 600;
58+
height: 100%;
59+
}
60+
61+
button:hover {
62+
background: #0052a3;
63+
}
64+
65+
button:disabled {
66+
background: #cccccc;
67+
cursor: not-allowed;
68+
}
69+
70+
.editor textarea {
71+
width: 100%;
72+
height: 150px;
73+
padding: 12px;
74+
margin-bottom: 20px;
75+
font-family: monospace;
76+
border: 2px solid #ccc;
77+
border-radius: 4px;
78+
resize: vertical;
79+
}
80+
81+
#output {
82+
background: white;
83+
border: 2px solid #ddd;
84+
border-radius: 4px;
85+
padding: 20px;
86+
min-height: 300px;
87+
display: flex;
88+
align-items: center;
89+
justify-content: center;
90+
}
91+
92+
#output svg {
93+
max-width: 100%;
94+
max-height: 260px;
95+
}
96+
97+
.error {
98+
color: #cc0000;
99+
margin-top: 10px;
100+
font-size: 14px;
101+
min-height: 20px;
102+
}
103+
104+
.label {
105+
font-weight: 600;
106+
margin-bottom: 8px;
107+
color: #333;
108+
}
109+
110+
.progress {
111+
height: 4px;
112+
background: #eee;
113+
margin-bottom: 20px;
114+
border-radius: 2px;
115+
overflow: hidden;
116+
}
117+
118+
.progress-bar {
119+
height: 100%;
120+
background: #0066cc;
121+
width: 0%;
122+
transition: width 0.1s ease-out;
123+
}
124+
125+
#loadExampleLink {
126+
color: #0066cc;
127+
text-decoration: none;
128+
margin-right: 20px;
129+
}
130+
131+
#loadExampleLink:hover {
132+
text-decoration: underline;
133+
}
134+
135+
.top-controls {
136+
display: flex;
137+
align-items: center;
138+
margin-bottom: 8px;
139+
}
140+
</style>
141+
<title>Progressively render SVG</title>
142+
</head>
143+
<body>
144+
<h1>Progressively render SVG</h1>
145+
<div class="top-controls">
146+
<a href="https://gist.githubusercontent.com/simonw/aedecb93564af13ac1596810d40cac3c/raw/83e7f3be5b65bba61124684700fa7925d37c36c3/tiger.svg" id="loadExampleLink">Load example image</a>
147+
</div>
148+
<div class="input-group">
149+
<textarea id="fullSvg" placeholder="Paste SVG here"></textarea>
150+
<input type="number" id="duration" class="duration-input" value="5" min="0.1" step="0.1">
151+
<span class="duration-label">seconds</span>
152+
<button id="renderBtn">Render</button>
153+
</div>
154+
<div class="progress">
155+
<div class="progress-bar" id="progressBar"></div>
156+
</div>
157+
158+
<div class="label">Live editor:</div>
159+
<div class="editor">
160+
<textarea id="input" spellcheck="false"><svg><rect x="10" y="10" width="80" height="80" fill="blue"></rect><circle cx="120" cy="50" r="30" fill="red"</textarea>
161+
</div>
162+
163+
<div class="label">Live Preview:</div>
164+
<div id="output"></div>
165+
<div id="error" class="error"></div>
166+
167+
<script>
168+
function completeSVG(incompleteSVG) {
169+
if (!incompleteSVG || !incompleteSVG.trim()) {
170+
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"></svg>';
171+
}
172+
173+
const openTags = [];
174+
const tagRegex = /<\/?([a-zA-Z0-9]+)(\s+[^>]*)?>/g;
175+
let match;
176+
let currentPos = 0;
177+
let processedSVG = '';
178+
179+
while ((match = tagRegex.exec(incompleteSVG)) !== null) {
180+
const fullTag = match[0];
181+
const tagName = match[1];
182+
const isClosingTag = fullTag.startsWith('</');
183+
184+
processedSVG += incompleteSVG.slice(currentPos, match.index);
185+
processedSVG += fullTag;
186+
currentPos = tagRegex.lastIndex;
187+
188+
if (!isClosingTag && !fullTag.endsWith('/>')) {
189+
openTags.push(tagName);
190+
} else if (isClosingTag) {
191+
if (openTags.length > 0 && openTags[openTags.length - 1] === tagName) {
192+
openTags.pop();
193+
}
194+
}
195+
}
196+
197+
processedSVG += incompleteSVG.slice(currentPos);
198+
199+
if (!incompleteSVG.includes('<svg')) {
200+
processedSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100">' + processedSVG;
201+
openTags.unshift('svg');
202+
}
203+
204+
while (openTags.length > 0) {
205+
const tagName = openTags.pop();
206+
processedSVG += `</${tagName}>`;
207+
}
208+
209+
return processedSVG;
210+
}
211+
212+
const input = document.getElementById('input');
213+
const fullSvg = document.getElementById('fullSvg');
214+
const duration = document.getElementById('duration');
215+
const renderBtn = document.getElementById('renderBtn');
216+
const output = document.getElementById('output');
217+
const error = document.getElementById('error');
218+
const progressBar = document.getElementById('progressBar');
219+
const loadExampleLink = document.getElementById('loadExampleLink');
220+
221+
// Prevent editor updates during manual editing
222+
let isManualEdit = false;
223+
input.addEventListener('input', () => {
224+
if (!isManualEdit) {
225+
updateSVG();
226+
}
227+
});
228+
229+
function updateSVG() {
230+
try {
231+
const completed = completeSVG(input.value);
232+
output.innerHTML = completed;
233+
error.textContent = '';
234+
} catch (e) {
235+
error.textContent = 'Error: ' + e.message;
236+
}
237+
}
238+
239+
function animateSVG(svgString) {
240+
const durationMs = Math.max(100, duration.value * 1000); // Minimum 0.1 seconds
241+
const interval = 100; // 100ms steps
242+
const steps = durationMs / interval;
243+
let currentStep = 0;
244+
245+
// Disable the render button during animation
246+
renderBtn.disabled = true;
247+
248+
// Clear any existing animation
249+
if (window.currentAnimation) {
250+
clearInterval(window.currentAnimation);
251+
}
252+
253+
window.currentAnimation = setInterval(() => {
254+
currentStep++;
255+
const progress = currentStep / steps;
256+
const charCount = Math.floor(svgString.length * progress);
257+
258+
// Update progress bar
259+
progressBar.style.width = `${progress * 100}%`;
260+
261+
// Get substring and complete it
262+
const partial = svgString.substring(0, charCount);
263+
const completed = completeSVG(partial);
264+
265+
// Update both the preview and editor
266+
output.innerHTML = completed;
267+
isManualEdit = true; // Prevent recursive updates
268+
input.value = partial;
269+
270+
// Scroll editor to bottom
271+
input.scrollTop = input.scrollHeight;
272+
273+
isManualEdit = false;
274+
275+
// End animation when complete
276+
if (currentStep >= steps) {
277+
clearInterval(window.currentAnimation);
278+
renderBtn.disabled = false;
279+
progressBar.style.width = '0%';
280+
}
281+
}, interval);
282+
}
283+
284+
// Handle paste and input events on the fullSvg textarea
285+
fullSvg.addEventListener('input', () => {
286+
// Clear the editor and preview
287+
input.value = '';
288+
output.innerHTML = '';
289+
error.textContent = '';
290+
});
291+
292+
renderBtn.addEventListener('click', () => {
293+
if (fullSvg.value.trim()) {
294+
animateSVG(fullSvg.value);
295+
}
296+
});
297+
298+
// Load example image
299+
loadExampleLink.addEventListener('click', async (e) => {
300+
e.preventDefault();
301+
try {
302+
const response = await fetch(loadExampleLink.href);
303+
if (!response.ok) throw new Error('Network response was not ok');
304+
const svgText = await response.text();
305+
fullSvg.value = svgText;
306+
error.textContent = '';
307+
// Clear the editor and preview
308+
input.value = '';
309+
output.innerHTML = '';
310+
} catch (error) {
311+
console.error('Error loading example:', error);
312+
error.textContent = 'Error loading example: ' + error.message;
313+
}
314+
});
315+
316+
// Initial render
317+
updateSVG();
318+
319+
// Prevent negative durations
320+
duration.addEventListener('input', () => {
321+
if (duration.value < 0.1) duration.value = 0.1;
322+
});
323+
</script>
324+
</body>
325+
</html>

0 commit comments

Comments
 (0)