create-demo-media.mjs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import { chromium } from '@playwright/test'
  2. import { mkdir, writeFile } from 'node:fs/promises'
  3. // Offline fixtures for photo guidance and silent explanation playback.
  4. const browser = await chromium.launch({ channel: 'chrome', headless: true })
  5. const page = await browser.newPage()
  6. await page.goto('about:blank')
  7. await mkdir('src/assets', { recursive: true })
  8. try {
  9. for (const answer of [false, true]) {
  10. const png = await page.evaluate(
  11. ({ answer }) => {
  12. const c = document.createElement('canvas')
  13. c.width = 720
  14. c.height = 960
  15. const x = c.getContext('2d')
  16. x.fillStyle = '#fff'
  17. x.fillRect(0, 0, 720, 960)
  18. x.strokeStyle = '#50425e'
  19. x.lineWidth = 2
  20. x.strokeRect(24, 24, 672, 912)
  21. x.fillStyle = '#49315e'
  22. x.textAlign = 'center'
  23. x.font = 'bold 32px sans-serif'
  24. x.fillText(answer ? '知了数学 · 答题卡' : '知了数学 · 数学练习卷', 360, 80)
  25. x.font = '22px sans-serif'
  26. x.fillText('二次函数专项练习', 360, 128)
  27. x.textAlign = 'left'
  28. x.fillStyle = '#5e5667'
  29. x.font = '18px sans-serif'
  30. x.fillText('姓名:李一 班级:初二 日期:2026/10/26', 54, 180)
  31. if (answer) {
  32. for (let r = 0; r < 15; r++)
  33. for (let col = 0; col < 15; col++)
  34. if ((r * 7 + col * 3) % 5 < 3) x.fillRect(588 + col * 4, 200 + r * 4, 4, 4)
  35. x.font = '16px sans-serif'
  36. x.fillText('编号:261026000030001', 54, 220)
  37. }
  38. const rows = answer
  39. ? [
  40. ['1. 选择题(请清晰填涂)', '1. [ A ] [ B ] [ ● ] [ D ]', '2. [ ● ] [ B ] [ C ] [ D ]'],
  41. ['2. 填空题', '交点坐标:(1, 0) 和 (3, 0)', '请保留完整作答区域'],
  42. ['3. 解答题(保留步骤)', '解:设 y = a(x − 1)² + 4', '代入 (0, 3),得 a = −1', '所以 y = −(x − 1)² + 4']
  43. ]
  44. : [
  45. [
  46. '1. 函数图像平移(选择题)',
  47. '将 y = −(x − 2)² + 3 平移至 y = −(x + 1)² − 2。',
  48. 'A. 右移3,上移5 B. 左移3,上移5',
  49. 'C. 左移3,下移5 D. 右移3,下移5'
  50. ],
  51. [
  52. '2. 二次函数与坐标轴(填空题)',
  53. '求 y = x² − 4x + 3 与 x 轴的交点坐标。',
  54. '答:________________________________'
  55. ],
  56. [
  57. '3. 抛物线解析式(解答题)',
  58. '已知顶点 (1, 4),且过点 (0, 3)。',
  59. '求抛物线的解析式及递减区间。',
  60. '请写出解题步骤。'
  61. ]
  62. ]
  63. rows.forEach((lines, n) => {
  64. const top = 280 + n * 195
  65. x.strokeStyle = '#ded6e7'
  66. x.strokeRect(48, top - 28, 624, 175)
  67. lines.forEach((line, i) => {
  68. x.font = i === 0 ? 'bold 21px sans-serif' : '18px sans-serif'
  69. x.fillStyle = answer && i > 0 ? '#33598a' : '#493e53'
  70. x.fillText(line, 64, top + i * 37)
  71. })
  72. })
  73. x.font = '17px sans-serif'
  74. x.fillStyle = '#95859f'
  75. x.fillText('■ 保留四周边缘及完整题目', 52, 914)
  76. x.fillText('第 1 页 · 页面示例', 490, 914)
  77. return c.toDataURL('image/png').split(',')[1]
  78. },
  79. { answer }
  80. )
  81. await writeFile(`src/assets/sample-${answer ? 'answer' : 'paper'}.png`, Buffer.from(png, 'base64'))
  82. }
  83. const clips = [
  84. [
  85. ['函数图像平移', '顶点 (2, 3) → (−1, −2)'],
  86. ['横坐标减少 3', '向左平移 3 个单位'],
  87. ['纵坐标减少 5', '向下平移 5 个单位'],
  88. ['参考答案:C', '向左平移3个单位,向下平移5个单位']
  89. ],
  90. [
  91. ['二次函数交点问题', 'y = x² − 4x + 3'],
  92. ['令 y = 0', 'x² − 4x + 3 = 0'],
  93. ['因式分解', '(x − 1)(x − 3) = 0'],
  94. ['参考答案', '(1, 0) 和 (3, 0)']
  95. ],
  96. [
  97. ['已知顶点 (1, 4)', '设 y = a(x − 1)² + 4'],
  98. ['代入 (0, 3)', '3 = a + 4,所以 a = −1'],
  99. ['得到解析式', 'y = −(x − 1)² + 4'],
  100. ['开口向下,对称轴 x = 1', '当 x > 1 时,函数递减']
  101. ],
  102. [
  103. ['一元二次方程', 'x² − 5x + 6 = 0'],
  104. ['因式分解', '(x − 2)(x − 3) = 0'],
  105. ['分别求解', 'x − 2 = 0 或 x − 3 = 0'],
  106. ['参考答案:A', 'x₁ = 2,x₂ = 3']
  107. ]
  108. ]
  109. for (let i = 0; i < clips.length; i++) {
  110. const data = await page.evaluate(async (steps) => {
  111. const mime = ['video/mp4;codecs=avc1.42001E', 'video/mp4'].find((v) => MediaRecorder.isTypeSupported(v))
  112. if (!mime) throw new Error('Chrome does not support MP4 recording')
  113. const canvas = document.createElement('canvas')
  114. canvas.width = 800
  115. canvas.height = 500
  116. const x = canvas.getContext('2d')
  117. const stream = canvas.captureStream(10)
  118. const recorder = new MediaRecorder(stream, { mimeType: mime, videoBitsPerSecond: 200000 })
  119. const chunks = []
  120. const start = performance.now()
  121. const draw = () => {
  122. const seconds = (performance.now() - start) / 1000
  123. const step = steps[Math.min(3, Math.floor(seconds / 2))]
  124. x.fillStyle = '#271a3b'
  125. x.fillRect(0, 0, 800, 500)
  126. x.fillStyle = '#bba1e5'
  127. x.font = '22px sans-serif'
  128. x.fillText('知了数学 · 题目讲解示范', 42, 62)
  129. x.fillStyle = '#fff'
  130. x.font = 'bold 30px sans-serif'
  131. x.fillText(step[0], 42, 170)
  132. x.font = '30px sans-serif'
  133. x.fillText(step[1], 42, 270)
  134. x.fillStyle = '#bdacce'
  135. x.font = '20px sans-serif'
  136. x.fillText(`第 ${Math.min(4, Math.floor(seconds / 2) + 1)} 步 / 共 4 步`, 42, 390)
  137. x.fillStyle = '#a479ea'
  138. x.fillRect(42, 440, Math.min(716, (seconds / 8) * 716), 6)
  139. }
  140. draw()
  141. const done = new Promise((resolve) => {
  142. recorder.onstop = () => resolve(new Blob(chunks, { type: mime }))
  143. recorder.ondataavailable = (e) => {
  144. if (e.data.size) chunks.push(e.data)
  145. }
  146. })
  147. recorder.start()
  148. const interval = setInterval(draw, 100)
  149. await new Promise((resolve) => setTimeout(resolve, 8100))
  150. recorder.stop()
  151. clearInterval(interval)
  152. const blob = await done
  153. stream.getTracks().forEach((t) => t.stop())
  154. return await new Promise((resolve) => {
  155. const reader = new FileReader()
  156. reader.onload = () => resolve(String(reader.result).split(',')[1])
  157. reader.readAsDataURL(blob)
  158. })
  159. }, clips[i])
  160. await writeFile(`src/assets/explanation-${i}.mp4`, Buffer.from(data, 'base64'))
  161. }
  162. } finally {
  163. await browser.close()
  164. }