This commit is contained in:
Rene Kievits
2025-12-18 01:30:52 +01:00
commit 6438660b03
78 changed files with 14230 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
<template lang="pug">
.svg-container(:class="{ loading: loading }")
svg.kanji-svg(
v-if="!loading"
viewBox="0 0 109 109"
width="100%"
height="100%"
)
g(v-for="(stroke, i) in strokes" :key="i")
path.stroke-path(
:d="stroke.d"
:class="{\
'animating': isPlaying && currentStrokeIdx === i,\
'hidden': isPlaying && currentStrokeIdx < i,\
'drawn': isPlaying && currentStrokeIdx > i\
}"
:style="{\
'--len': stroke.len,\
'--duration': (stroke.len * 0.02) + 's'\
}"
)
g(v-show="!isPlaying || currentStrokeIdx > i")
circle.stroke-start-circle(
v-if="stroke.start"
:cx="stroke.start.x"
:cy="stroke.start.y"
r="3.5"
)
text.stroke-number(
v-if="stroke.start"
:x="stroke.start.x"
:y="stroke.start.y + 0.5"
) {{ i + 1 }}
path.stroke-arrow-line(
v-if="stroke.arrow"
d="M -7 0 L 2 0 M -1 -3 L 2 0 L -1 3"
:transform="`translate(${stroke.arrow.x}, ${stroke.arrow.y}) rotate(${stroke.arrow.angle})`"
)
.loading-spinner(v-else) {{ $t('review.loading') }}
button.play-btn(
v-if="!loading && !isPlaying"
@click="playAnimation"
)
svg(viewBox="0 0 24 24" fill="currentColor")
path(d="M8 5v14l11-7z")
</template>
<script setup>
import { ref, watch, onMounted } from 'vue';
const props = defineProps({
char: {
type: String,
required: true
}
});
const strokes = ref([]);
const loading = ref(true);
const isPlaying = ref(false);
const currentStrokeIdx = ref(-1);
onMounted(() => {
if (props.char) loadData(props.char);
});
watch(() => props.char, (newChar) => {
if (newChar) loadData(newChar);
});
async function loadData(char) {
loading.value = true;
isPlaying.value = false;
strokes.value = [];
const hex = char.charCodeAt(0).toString(16).padStart(5, '0');
try {
const res = await fetch(`https://raw.githubusercontent.com/KanjiVG/kanjivg/master/kanji/${hex}.svg`);
const txt = await res.text();
const parser = new DOMParser();
const doc = parser.parseFromString(txt, "image/svg+xml");
const rawPaths = Array.from(doc.getElementsByTagName("path")).map(p => p.getAttribute("d"));
strokes.value = rawPaths.map(d => {
const data = { d, start: null, arrow: null, len: 0 };
const tempPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
tempPath.setAttribute("d", d);
try {
data.len = tempPath.getTotalLength();
} catch (e) { data.len = 100; }
const startMatch = d.match(/[Mm]\s*([\d.]+)[,\s]([\d.]+)/);
if (startMatch) {
data.start = { x: parseFloat(startMatch[1]), y: parseFloat(startMatch[2]) };
}
try {
const mid = tempPath.getPointAtLength(data.len / 2);
const prev = tempPath.getPointAtLength(Math.max(0, (data.len / 2) - 1));
const angle = Math.atan2(mid.y - prev.y, mid.x - prev.x) * (180 / Math.PI);
data.arrow = { x: mid.x, y: mid.y, angle };
} catch (e) { console.error(e) }
return data;
});
} catch (e) {
console.error("SVG Load Failed", e);
} finally {
loading.value = false;
}
}
async function playAnimation() {
if (isPlaying.value) return;
isPlaying.value = true;
currentStrokeIdx.value = -1;
await new Promise(r => setTimeout(r, 200));
for (let i = 0; i < strokes.value.length; i++) {
currentStrokeIdx.value = i;
const duration = strokes.value[i].len * 20;
await new Promise(r => setTimeout(r, duration + 100));
}
await new Promise(r => setTimeout(r, 500));
isPlaying.value = false;
}
</script>
<style lang="scss" src="@/styles/components/_kanji.scss" scoped></style>