add new lesson mode and started code refraction
This commit is contained in:
@@ -1,138 +1,151 @@
|
||||
<template lang="pug">
|
||||
.svg-container(:class="{ loading: loading }")
|
||||
.svg-container(
|
||||
:class="{ 'canvas-mode': mode === 'animate', 'hero-mode': mode === 'hero' }"
|
||||
)
|
||||
svg.kanji-svg(
|
||||
v-if="!loading"
|
||||
v-show="!loading"
|
||||
viewBox="0 0 109 109"
|
||||
width="100%"
|
||||
height="100%"
|
||||
)
|
||||
g(v-if="mode === 'animate'")
|
||||
path.stroke-ghost(
|
||||
v-for="(stroke, i) in strokes"
|
||||
:key="'ghost-'+i"
|
||||
:d="stroke.d"
|
||||
)
|
||||
|
||||
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'\
|
||||
}"
|
||||
:class="getStrokeClass(i)"
|
||||
:style="getStrokeStyle(stroke)"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
g(v-if="mode === 'animate' && (!isPlaying || currentStrokeIdx > -1)")
|
||||
g(v-for="(stroke, i) in strokes" :key="'anno-'+i")
|
||||
g(v-show="!isPlaying || currentStrokeIdx >= i")
|
||||
path.stroke-arrow-line(
|
||||
v-if="isPlaying && currentStrokeIdx === i && stroke.arrow"
|
||||
d="M -7 0 L 2 0 M -1 -3 L 2 0 L -1 3"
|
||||
:transform="getArrowTransform(stroke.arrow)"
|
||||
)
|
||||
|
||||
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') }}
|
||||
g.stroke-badge-group(
|
||||
v-if="stroke.start"
|
||||
:transform="`translate(${stroke.start.x}, ${stroke.start.y})`"
|
||||
)
|
||||
circle.stroke-badge-bg(r="4")
|
||||
text.stroke-badge-text(
|
||||
dy="0.5"
|
||||
) {{ i + 1 }}
|
||||
|
||||
button.play-btn(
|
||||
v-if="!loading && !isPlaying"
|
||||
@click="playAnimation"
|
||||
v-if="mode === 'animate' && !loading && !isPlaying"
|
||||
@click.stop="playAnimation"
|
||||
)
|
||||
svg(viewBox="0 0 24 24" fill="currentColor")
|
||||
path(d="M8 5v14l11-7z")
|
||||
path(d="M8,5.14V19.14L19,12.14L8,5.14Z")
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
char: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
char: { type: String, required: true },
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'animate',
|
||||
validator: (v) => ['hero', 'animate'].includes(v),
|
||||
},
|
||||
});
|
||||
|
||||
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 = [];
|
||||
loading.value = true;
|
||||
isPlaying.value = false;
|
||||
strokes.value = [];
|
||||
const hex = char.charCodeAt(0).toString(16).padStart(5, '0');
|
||||
|
||||
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");
|
||||
try {
|
||||
const baseUrl = 'https://raw.githubusercontent.com/KanjiVG/kanjivg/master/kanji';
|
||||
const res = await fetch(`${baseUrl}/${hex}.svg`);
|
||||
|
||||
const rawPaths = Array.from(doc.getElementsByTagName("path")).map(p => p.getAttribute("d"));
|
||||
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 };
|
||||
strokes.value = rawPaths.map((d) => {
|
||||
const data = {
|
||||
d, start: null, arrow: null, len: 0, duration: 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; }
|
||||
data.duration = Math.floor(data.len * 20);
|
||||
|
||||
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]) };
|
||||
|
||||
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(e); } finally { loading.value = false; }
|
||||
}
|
||||
|
||||
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) }
|
||||
function getArrowTransform(arrow) {
|
||||
if (!arrow) return '';
|
||||
return `translate(${arrow.x}, ${arrow.y}) rotate(${arrow.angle})`;
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
function getStrokeClass(i) {
|
||||
if (props.mode === 'hero') return 'drawn';
|
||||
|
||||
} catch (e) {
|
||||
console.error("SVG Load Failed", e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
if (isPlaying.value) {
|
||||
if (currentStrokeIdx.value === i) return 'animating';
|
||||
if (currentStrokeIdx.value > i) return 'drawn';
|
||||
return 'hidden';
|
||||
}
|
||||
return 'drawn';
|
||||
}
|
||||
|
||||
function getStrokeStyle(stroke) {
|
||||
if (props.mode === 'hero') return {};
|
||||
return { '--len': stroke.len, '--duration': `${stroke.duration}ms` };
|
||||
}
|
||||
|
||||
async function playAnimation() {
|
||||
if (isPlaying.value) return;
|
||||
isPlaying.value = true;
|
||||
currentStrokeIdx.value = -1;
|
||||
if (isPlaying.value) return;
|
||||
isPlaying.value = true;
|
||||
currentStrokeIdx.value = -1;
|
||||
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
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));
|
||||
}
|
||||
for (let i = 0; i < strokes.value.length; i += 1) {
|
||||
currentStrokeIdx.value = i;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((r) => { setTimeout(r, strokes.value[i].duration); });
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((r) => { setTimeout(r, 100); });
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
isPlaying.value = false;
|
||||
await new Promise((r) => { setTimeout(r, 500); });
|
||||
isPlaying.value = false;
|
||||
}
|
||||
|
||||
defineExpose({ playAnimation });
|
||||
onMounted(() => { if (props.char) loadData(props.char); });
|
||||
watch(() => props.char, (n) => { if (n) loadData(n); });
|
||||
</script>
|
||||
|
||||
<style lang="scss" src="@/styles/components/_kanji.scss" scoped></style>
|
||||
|
||||
Reference in New Issue
Block a user