| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- import { chromium } from '@playwright/test'
- import { mkdir, writeFile } from 'node:fs/promises'
- // Offline fixtures for photo guidance and silent explanation playback.
- const browser = await chromium.launch({ channel: 'chrome', headless: true })
- const page = await browser.newPage()
- await page.goto('about:blank')
- await mkdir('src/assets', { recursive: true })
- try {
- for (const answer of [false, true]) {
- const png = await page.evaluate(
- ({ answer }) => {
- const c = document.createElement('canvas')
- c.width = 720
- c.height = 960
- const x = c.getContext('2d')
- x.fillStyle = '#fff'
- x.fillRect(0, 0, 720, 960)
- x.strokeStyle = '#50425e'
- x.lineWidth = 2
- x.strokeRect(24, 24, 672, 912)
- x.fillStyle = '#49315e'
- x.textAlign = 'center'
- x.font = 'bold 32px sans-serif'
- x.fillText(answer ? '知了数学 · 答题卡' : '知了数学 · 数学练习卷', 360, 80)
- x.font = '22px sans-serif'
- x.fillText('二次函数专项练习', 360, 128)
- x.textAlign = 'left'
- x.fillStyle = '#5e5667'
- x.font = '18px sans-serif'
- x.fillText('姓名:李一 班级:初二 日期:2026/10/26', 54, 180)
- if (answer) {
- for (let r = 0; r < 15; r++)
- for (let col = 0; col < 15; col++)
- if ((r * 7 + col * 3) % 5 < 3) x.fillRect(588 + col * 4, 200 + r * 4, 4, 4)
- x.font = '16px sans-serif'
- x.fillText('编号:261026000030001', 54, 220)
- }
- const rows = answer
- ? [
- ['1. 选择题(请清晰填涂)', '1. [ A ] [ B ] [ ● ] [ D ]', '2. [ ● ] [ B ] [ C ] [ D ]'],
- ['2. 填空题', '交点坐标:(1, 0) 和 (3, 0)', '请保留完整作答区域'],
- ['3. 解答题(保留步骤)', '解:设 y = a(x − 1)² + 4', '代入 (0, 3),得 a = −1', '所以 y = −(x − 1)² + 4']
- ]
- : [
- [
- '1. 函数图像平移(选择题)',
- '将 y = −(x − 2)² + 3 平移至 y = −(x + 1)² − 2。',
- 'A. 右移3,上移5 B. 左移3,上移5',
- 'C. 左移3,下移5 D. 右移3,下移5'
- ],
- [
- '2. 二次函数与坐标轴(填空题)',
- '求 y = x² − 4x + 3 与 x 轴的交点坐标。',
- '答:________________________________'
- ],
- [
- '3. 抛物线解析式(解答题)',
- '已知顶点 (1, 4),且过点 (0, 3)。',
- '求抛物线的解析式及递减区间。',
- '请写出解题步骤。'
- ]
- ]
- rows.forEach((lines, n) => {
- const top = 280 + n * 195
- x.strokeStyle = '#ded6e7'
- x.strokeRect(48, top - 28, 624, 175)
- lines.forEach((line, i) => {
- x.font = i === 0 ? 'bold 21px sans-serif' : '18px sans-serif'
- x.fillStyle = answer && i > 0 ? '#33598a' : '#493e53'
- x.fillText(line, 64, top + i * 37)
- })
- })
- x.font = '17px sans-serif'
- x.fillStyle = '#95859f'
- x.fillText('■ 保留四周边缘及完整题目', 52, 914)
- x.fillText('第 1 页 · 页面示例', 490, 914)
- return c.toDataURL('image/png').split(',')[1]
- },
- { answer }
- )
- await writeFile(`src/assets/sample-${answer ? 'answer' : 'paper'}.png`, Buffer.from(png, 'base64'))
- }
- const clips = [
- [
- ['函数图像平移', '顶点 (2, 3) → (−1, −2)'],
- ['横坐标减少 3', '向左平移 3 个单位'],
- ['纵坐标减少 5', '向下平移 5 个单位'],
- ['参考答案:C', '向左平移3个单位,向下平移5个单位']
- ],
- [
- ['二次函数交点问题', 'y = x² − 4x + 3'],
- ['令 y = 0', 'x² − 4x + 3 = 0'],
- ['因式分解', '(x − 1)(x − 3) = 0'],
- ['参考答案', '(1, 0) 和 (3, 0)']
- ],
- [
- ['已知顶点 (1, 4)', '设 y = a(x − 1)² + 4'],
- ['代入 (0, 3)', '3 = a + 4,所以 a = −1'],
- ['得到解析式', 'y = −(x − 1)² + 4'],
- ['开口向下,对称轴 x = 1', '当 x > 1 时,函数递减']
- ],
- [
- ['一元二次方程', 'x² − 5x + 6 = 0'],
- ['因式分解', '(x − 2)(x − 3) = 0'],
- ['分别求解', 'x − 2 = 0 或 x − 3 = 0'],
- ['参考答案:A', 'x₁ = 2,x₂ = 3']
- ]
- ]
- for (let i = 0; i < clips.length; i++) {
- const data = await page.evaluate(async (steps) => {
- const mime = ['video/mp4;codecs=avc1.42001E', 'video/mp4'].find((v) => MediaRecorder.isTypeSupported(v))
- if (!mime) throw new Error('Chrome does not support MP4 recording')
- const canvas = document.createElement('canvas')
- canvas.width = 800
- canvas.height = 500
- const x = canvas.getContext('2d')
- const stream = canvas.captureStream(10)
- const recorder = new MediaRecorder(stream, { mimeType: mime, videoBitsPerSecond: 200000 })
- const chunks = []
- const start = performance.now()
- const draw = () => {
- const seconds = (performance.now() - start) / 1000
- const step = steps[Math.min(3, Math.floor(seconds / 2))]
- x.fillStyle = '#271a3b'
- x.fillRect(0, 0, 800, 500)
- x.fillStyle = '#bba1e5'
- x.font = '22px sans-serif'
- x.fillText('知了数学 · 题目讲解示范', 42, 62)
- x.fillStyle = '#fff'
- x.font = 'bold 30px sans-serif'
- x.fillText(step[0], 42, 170)
- x.font = '30px sans-serif'
- x.fillText(step[1], 42, 270)
- x.fillStyle = '#bdacce'
- x.font = '20px sans-serif'
- x.fillText(`第 ${Math.min(4, Math.floor(seconds / 2) + 1)} 步 / 共 4 步`, 42, 390)
- x.fillStyle = '#a479ea'
- x.fillRect(42, 440, Math.min(716, (seconds / 8) * 716), 6)
- }
- draw()
- const done = new Promise((resolve) => {
- recorder.onstop = () => resolve(new Blob(chunks, { type: mime }))
- recorder.ondataavailable = (e) => {
- if (e.data.size) chunks.push(e.data)
- }
- })
- recorder.start()
- const interval = setInterval(draw, 100)
- await new Promise((resolve) => setTimeout(resolve, 8100))
- recorder.stop()
- clearInterval(interval)
- const blob = await done
- stream.getTracks().forEach((t) => t.stop())
- return await new Promise((resolve) => {
- const reader = new FileReader()
- reader.onload = () => resolve(String(reader.result).split(',')[1])
- reader.readAsDataURL(blob)
- })
- }, clips[i])
- await writeFile(`src/assets/explanation-${i}.mp4`, Buffer.from(data, 'base64'))
- }
- } finally {
- await browser.close()
- }
|