mirror of
https://github.com/leporello-js/leporello-js
synced 2026-01-13 13:04:30 -08:00
canvas
This commit is contained in:
68
docs/examples/animated_fractal_tree/animated_fractal_tree.js
Normal file
68
docs/examples/animated_fractal_tree/animated_fractal_tree.js
Normal file
@@ -0,0 +1,68 @@
|
||||
// Original source: http://bricault.mit.edu/recursive-drawing
|
||||
// Author: Sarah Bricault
|
||||
|
||||
// Canvas setup
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 700
|
||||
canvas.height = 700
|
||||
document.body.appendChild(canvas)
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.translate(canvas.width / 2, canvas.height)
|
||||
|
||||
// Draw a tree
|
||||
await fractalTreeBasic({totalIterations: 10, basicLength: 10, rotate: 25})
|
||||
|
||||
function sleep() {
|
||||
return new Promise(resolve => setTimeout(resolve, 3))
|
||||
}
|
||||
|
||||
async function fractalTreeBasic({totalIterations, basicLength, rotate}) {
|
||||
|
||||
// Draw the tree trunk
|
||||
const trunkLength = basicLength * 2 * Math.pow(1.2, totalIterations + 1)
|
||||
const width = Math.pow(totalIterations, 0.6)
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, 0)
|
||||
ctx.lineTo(0, - trunkLength)
|
||||
ctx.lineWidth = width
|
||||
ctx.strokeStyle = 'black'
|
||||
ctx.stroke()
|
||||
|
||||
await drawBranch(90, [0, - trunkLength], totalIterations + 1)
|
||||
|
||||
async function drawBranch(angle, startPoint, iterations) {
|
||||
const len = basicLength * Math.pow(1.2, iterations)
|
||||
|
||||
const width = Math.pow(iterations, 0.6)
|
||||
|
||||
const red = Math.floor(255 - (iterations / totalIterations) * 255)
|
||||
const green = 0
|
||||
const blue = Math.floor( 255 - (iterations / totalIterations) * 255)
|
||||
const color = `rgb(${red}, ${green}, ${blue})`
|
||||
|
||||
const x1 = startPoint[0]
|
||||
const y1 = startPoint[1]
|
||||
|
||||
const y2 = y1 - len * Math.sin((angle * Math.PI) / 180)
|
||||
const x2 = x1 + len * Math.cos((angle * Math.PI) / 180)
|
||||
|
||||
console.log('draw branch', x1, y1, x2, y2)
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x1, y1)
|
||||
ctx.lineTo(x2, y2)
|
||||
ctx.lineWidth = width
|
||||
ctx.strokeStyle = color
|
||||
ctx.stroke()
|
||||
|
||||
await sleep()
|
||||
|
||||
if (iterations - 1 > 0) {
|
||||
// draw left branch
|
||||
await drawBranch(angle + rotate, [x2, y2], iterations - 1)
|
||||
// draw right branch
|
||||
await drawBranch(angle - rotate, [x2, y2], iterations - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
98
docs/examples/canvas_animation_bubbles/bubbles.js
Normal file
98
docs/examples/canvas_animation_bubbles/bubbles.js
Normal file
@@ -0,0 +1,98 @@
|
||||
// Original source:
|
||||
// https://www.freecodecamp.org/news/how-to-create-animated-bubbles-with-html5-canvas-and-javascript/
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.style.backgroundColor = '#00b4ff'
|
||||
document.body.appendChild(canvas)
|
||||
canvas.width = window.innerWidth
|
||||
canvas.height = window.innerHeight
|
||||
|
||||
const context = canvas.getContext("2d")
|
||||
|
||||
context.font = "30px Arial"
|
||||
context.textAlign = 'center'
|
||||
context.fillStyle = 'white'
|
||||
context.fillText('Click to spawn bubbles', canvas.width/2, canvas.height/2)
|
||||
|
||||
let circles = []
|
||||
|
||||
function draw(circle) {
|
||||
context.beginPath()
|
||||
context.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI)
|
||||
context.strokeStyle = `hsl(${circle.hue} 100% 50%)`
|
||||
context.stroke()
|
||||
|
||||
const gradient = context.createRadialGradient(
|
||||
circle.x,
|
||||
circle.y,
|
||||
1,
|
||||
circle.x + 0.5,
|
||||
circle.y + 0.5,
|
||||
circle.radius
|
||||
)
|
||||
|
||||
gradient.addColorStop(0.3, "rgba(255, 255, 255, 0.3)")
|
||||
gradient.addColorStop(0.95, "#e7feff")
|
||||
|
||||
context.fillStyle = gradient
|
||||
context.fill()
|
||||
}
|
||||
|
||||
function move(circle, timeDelta) {
|
||||
circle.x = circle.x + timeDelta*circle.dx
|
||||
circle.y = circle.y - timeDelta*circle.dy
|
||||
}
|
||||
|
||||
let intervalId
|
||||
|
||||
function startAnimation() {
|
||||
if(intervalId == null) {
|
||||
intervalId = setInterval(animate, 20)
|
||||
}
|
||||
}
|
||||
|
||||
function stopAnimation() {
|
||||
if(intervalId != null) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = null
|
||||
}
|
||||
}
|
||||
|
||||
let prevFrameTime
|
||||
|
||||
const animate = () => {
|
||||
const now = Date.now()
|
||||
const timeDelta = prevFrameTime == null ? 0 : now - prevFrameTime
|
||||
prevFrameTime = now
|
||||
|
||||
if(circles.length == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
context.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
circles.forEach(circle => {
|
||||
move(circle, timeDelta)
|
||||
draw(circle)
|
||||
})
|
||||
}
|
||||
|
||||
const createCircles = (event) => {
|
||||
startAnimation()
|
||||
|
||||
circles = circles.concat(Array.from({length: 50}, () => (
|
||||
{
|
||||
x: event.pageX,
|
||||
y: event.pageY,
|
||||
radius: Math.random() * 50,
|
||||
dx: Math.random() * 0.3,
|
||||
dy: Math.random() * 0.7,
|
||||
hue: 200,
|
||||
}
|
||||
)))
|
||||
}
|
||||
|
||||
canvas.addEventListener("click", createCircles)
|
||||
|
||||
window.onfocus = startAnimation
|
||||
window.onblur = stopAnimation
|
||||
62
docs/examples/fractal_tree/fractal_tree.js
Normal file
62
docs/examples/fractal_tree/fractal_tree.js
Normal file
@@ -0,0 +1,62 @@
|
||||
// Original source: http://bricault.mit.edu/recursive-drawing
|
||||
// Author: Sarah Bricault
|
||||
|
||||
// Canvas setup
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 700
|
||||
canvas.height = 700
|
||||
document.body.appendChild(canvas)
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.translate(canvas.width / 2, canvas.height)
|
||||
|
||||
// Draw a tree
|
||||
fractalTreeBasic({totalIterations: 10, basicLength: 10, rotate: 25})
|
||||
|
||||
function fractalTreeBasic({totalIterations, basicLength, rotate}) {
|
||||
|
||||
// Draw the tree trunk
|
||||
const trunkLength = basicLength * 2 * Math.pow(1.2, totalIterations + 1)
|
||||
const width = Math.pow(totalIterations, 0.6)
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, 0)
|
||||
ctx.lineTo(0, - trunkLength)
|
||||
ctx.lineWidth = width
|
||||
ctx.strokeStyle = 'black'
|
||||
ctx.stroke()
|
||||
|
||||
drawBranch(90, [0, - trunkLength], totalIterations + 1)
|
||||
|
||||
function drawBranch(angle, startPoint, iterations) {
|
||||
const len = basicLength * Math.pow(1.2, iterations)
|
||||
|
||||
const width = Math.pow(iterations, 0.6)
|
||||
|
||||
const red = Math.floor(255 - (iterations / totalIterations) * 255)
|
||||
const green = 0
|
||||
const blue = Math.floor( 255 - (iterations / totalIterations) * 255)
|
||||
const color = `rgb(${red}, ${green}, ${blue})`
|
||||
|
||||
const x1 = startPoint[0]
|
||||
const y1 = startPoint[1]
|
||||
|
||||
const y2 = y1 - len * Math.sin((angle * Math.PI) / 180)
|
||||
const x2 = x1 + len * Math.cos((angle * Math.PI) / 180)
|
||||
|
||||
console.log('draw branch', x1, y1, x2, y2)
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x1, y1)
|
||||
ctx.lineTo(x2, y2)
|
||||
ctx.lineWidth = width
|
||||
ctx.strokeStyle = color
|
||||
ctx.stroke()
|
||||
|
||||
if (iterations - 1 > 0) {
|
||||
// draw left branch
|
||||
drawBranch(angle + rotate, [x2, y2], iterations - 1)
|
||||
// draw right branch
|
||||
drawBranch(angle - rotate, [x2, y2], iterations - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -566,19 +566,26 @@ const select_and_toggle_expanded = (state, id) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const expand_path = (state, node) => ({
|
||||
...state,
|
||||
calltree_node_is_expanded: {
|
||||
...state.calltree_node_is_expanded,
|
||||
...Object.fromEntries(
|
||||
path_to_root(state.calltree, node)
|
||||
.map(n => [n.id, true])
|
||||
),
|
||||
// Also expand node, since it is not included in
|
||||
// path_to_root
|
||||
[node.id]: true,
|
||||
export const expand_path = (state, node) => {
|
||||
if(state.calltree_node_is_expanded?.[node.id]) {
|
||||
return state
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return {
|
||||
...state,
|
||||
calltree_node_is_expanded: {
|
||||
...state.calltree_node_is_expanded,
|
||||
...Object.fromEntries(
|
||||
path_to_root(state.calltree, node)
|
||||
.map(n => [n.id, true])
|
||||
),
|
||||
// Also expand node, since it is not included in
|
||||
// path_to_root
|
||||
[node.id]: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const initial_calltree_node = state => {
|
||||
const root = root_calltree_node(state)
|
||||
|
||||
144
src/canvas.js
Normal file
144
src/canvas.js
Normal file
@@ -0,0 +1,144 @@
|
||||
// TODO time-travel for canvas ImageData
|
||||
|
||||
import {abort_replay} from './runtime/record_io.js'
|
||||
import {set_record_call} from './runtime/runtime.js'
|
||||
import {is_expandable} from './calltree.js'
|
||||
|
||||
const context_reset = globalThis?.CanvasRenderingContext2D?.prototype?.reset
|
||||
|
||||
function reset(context) {
|
||||
if(context_reset != null) {
|
||||
context_reset.call(context)
|
||||
} else {
|
||||
// For older browsers, `reset` may be not available
|
||||
// changing width does the same as `reset`
|
||||
// see https://stackoverflow.com/a/45871243/795038
|
||||
context.canvas.width = context.canvas.width + 0
|
||||
}
|
||||
}
|
||||
|
||||
function canvas_reset(canvas_ops) {
|
||||
for(let context of canvas_ops.contexts) {
|
||||
reset(context)
|
||||
}
|
||||
}
|
||||
|
||||
export function apply_canvas_patches(window) {
|
||||
const proto = window?.CanvasRenderingContext2D?.prototype
|
||||
|
||||
if(proto == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const props = Object.getOwnPropertyDescriptors(proto)
|
||||
|
||||
Object.entries(props).forEach(([name, p]) => {
|
||||
if(p.value != null) {
|
||||
if(typeof(p.value) != 'function') {
|
||||
// At the moment this was written, all canvas values were functions
|
||||
return
|
||||
}
|
||||
const method = p.value
|
||||
proto[name] = {
|
||||
// declare function like this so it has `name` property set
|
||||
[name]() {
|
||||
const cxt = window.__cxt
|
||||
|
||||
set_record_call(cxt)
|
||||
|
||||
/*
|
||||
abort replay, because otherwise animated_fractal_tree would replay
|
||||
instantly (because setTimeout is in io_trace)
|
||||
*/
|
||||
if(!cxt.io_trace_is_recording && !cxt.is_recording_deferred_calls) {
|
||||
abort_replay(cxt)
|
||||
}
|
||||
|
||||
const version_number = ++cxt.version_counter
|
||||
|
||||
try {
|
||||
return method.apply(this, arguments)
|
||||
} finally {
|
||||
cxt.canvas_ops.contexts.add(this)
|
||||
cxt.canvas_ops.ops.push({
|
||||
canvas_context: this,
|
||||
method,
|
||||
version_number,
|
||||
args: arguments,
|
||||
})
|
||||
}
|
||||
}
|
||||
}[name]
|
||||
}
|
||||
|
||||
if(p.set != null) {
|
||||
const set_op = p.set
|
||||
Object.defineProperty(proto, name, {
|
||||
set(prop_value) {
|
||||
const cxt = window.__cxt
|
||||
|
||||
set_record_call(cxt)
|
||||
|
||||
if(!cxt.io_trace_is_recording && !cxt.is_recording_deferred_calls) {
|
||||
abort_replay(cxt)
|
||||
}
|
||||
|
||||
const version_number = ++cxt.version_counter
|
||||
|
||||
try {
|
||||
set_op.call(this, prop_value)
|
||||
} finally {
|
||||
cxt.canvas_ops.contexts.add(this)
|
||||
cxt.canvas_ops.ops.push({
|
||||
canvas_context: this,
|
||||
version_number,
|
||||
set_op,
|
||||
prop_value,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function replay_op(op) {
|
||||
if(op.method != null) {
|
||||
op.method.apply(op.canvas_context, op.args)
|
||||
} else if(op.set_op != null) {
|
||||
op.set_op.call(op.canvas_context, op.prop_value)
|
||||
} else {
|
||||
throw new Error('illegal op')
|
||||
}
|
||||
}
|
||||
|
||||
export function redraw_canvas(state, is_replay_all_canvs_ops) {
|
||||
if(state.calltree == null) {
|
||||
// code is invalid or not executed yet
|
||||
return
|
||||
}
|
||||
|
||||
const cxt = state.rt_cxt
|
||||
|
||||
if(cxt.canvas_ops.ops == null) {
|
||||
return
|
||||
}
|
||||
|
||||
canvas_reset(cxt.canvas_ops)
|
||||
|
||||
if(is_replay_all_canvs_ops) {
|
||||
for(let op of cxt.canvas_ops.ops) {
|
||||
replay_op(op)
|
||||
}
|
||||
} else {
|
||||
const current = state.current_calltree_node
|
||||
// replay all ops up to current_calltree_node, including
|
||||
const version_number = state.current_calltree_node.last_version_number
|
||||
for(let op of cxt.canvas_ops.ops) {
|
||||
if(op.version_number > version_number) {
|
||||
break
|
||||
}
|
||||
replay_op(op)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,10 +150,13 @@ export class CallTree {
|
||||
}
|
||||
this.state = state
|
||||
this.render_active(this.state.current_calltree_node, true)
|
||||
scrollIntoViewIfNeeded(
|
||||
this.container,
|
||||
this.node_to_el.get(this.state.current_calltree_node.id).getElementsByClassName('call_el')[0]
|
||||
)
|
||||
if(prev?.current_calltree_node != state.current_calltree_node) {
|
||||
// prevent scroll on adding deferred call
|
||||
scrollIntoViewIfNeeded(
|
||||
this.container,
|
||||
this.node_to_el.get(this.state.current_calltree_node.id).getElementsByClassName('call_el')[0]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
render_expand_node(prev_state, state) {
|
||||
|
||||
@@ -171,9 +171,13 @@ export class Editor {
|
||||
})
|
||||
}
|
||||
|
||||
has_value_explorer() {
|
||||
return this.value_explorer != null
|
||||
}
|
||||
|
||||
unembed_value_explorer() {
|
||||
if(this.value_explorer == null) {
|
||||
return
|
||||
if(!this.has_value_explorer()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const session = this.ace_editor.getSession()
|
||||
|
||||
@@ -6,6 +6,7 @@ import {Logs} from './logs.js'
|
||||
import {IO_Trace} from './io_trace.js'
|
||||
import {ShareDialog} from './share_dialog.js'
|
||||
import {el} from './domutils.js'
|
||||
import {redraw_canvas} from '../canvas.js'
|
||||
|
||||
export class UI {
|
||||
constructor(container, state){
|
||||
@@ -174,6 +175,27 @@ export class UI {
|
||||
this.render_current_module(state.current_module)
|
||||
|
||||
this.set_active_tab('calltree', true)
|
||||
|
||||
container.addEventListener('focusin', e => {
|
||||
const active = document.activeElement
|
||||
let is_focus_in_editor
|
||||
if(this.editor_container.contains(document.activeElement)) {
|
||||
if(this.editor.has_value_explorer()) {
|
||||
// depends on if we come to value explorer from editor or from debugger
|
||||
is_focus_in_editor = !this.debugger_container.contains(
|
||||
this.editor.value_explorer.return_to
|
||||
)
|
||||
} else {
|
||||
is_focus_in_editor = true
|
||||
}
|
||||
} else {
|
||||
is_focus_in_editor = false
|
||||
}
|
||||
if(this.prev_is_focus_in_editor != is_focus_in_editor) {
|
||||
this.prev_is_focus_in_editor= is_focus_in_editor
|
||||
redraw_canvas(get_state(), is_focus_in_editor)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
set_active_tab(tab_id, skip_focus = false) {
|
||||
|
||||
22
src/effects.js
vendored
22
src/effects.js
vendored
@@ -8,6 +8,7 @@ import {
|
||||
} from './calltree.js'
|
||||
import {current_cursor_position} from './calltree.js'
|
||||
import {exec, reload_app_window, FILES_ROOT} from './index.js'
|
||||
import {redraw_canvas} from './canvas.js'
|
||||
|
||||
// Imports in the context of `app_window`, so global variables in loaded
|
||||
// modules refer to that window's context
|
||||
@@ -139,7 +140,7 @@ export const render_initial_state = (ui, state, example) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const apply_side_effects = (prev, next, ui) => {
|
||||
export const apply_side_effects = (prev, next, ui, cmd) => {
|
||||
if(prev.project_dir != next.project_dir) {
|
||||
ui.files.render(next)
|
||||
}
|
||||
@@ -156,7 +157,6 @@ export const apply_side_effects = (prev, next, ui) => {
|
||||
if(prev.entrypoint != next.entrypoint) {
|
||||
localStorage.entrypoint = next.entrypoint
|
||||
}
|
||||
|
||||
if(prev.html_file != next.html_file) {
|
||||
localStorage.html_file = next.html_file
|
||||
}
|
||||
@@ -167,7 +167,8 @@ export const apply_side_effects = (prev, next, ui) => {
|
||||
ui.editor.switch_session(next.current_module)
|
||||
}
|
||||
|
||||
if(current_cursor_position(next) != ui.editor.get_cursor_position()) {
|
||||
// Do not set cursor position on_deferred_call, because editor may be in the middle of the edition operation
|
||||
if(current_cursor_position(next) != ui.editor.get_cursor_position() && cmd != 'on_deferred_call') {
|
||||
ui.editor.set_cursor_position(current_cursor_position(next))
|
||||
}
|
||||
|
||||
@@ -205,6 +206,9 @@ export const apply_side_effects = (prev, next, ui) => {
|
||||
||
|
||||
prev.calltree_changed_token != next.calltree_changed_token
|
||||
) {
|
||||
|
||||
// code finished executing
|
||||
|
||||
const is_loading =
|
||||
next.loading_external_imports_state != null
|
||||
||
|
||||
@@ -233,6 +237,8 @@ export const apply_side_effects = (prev, next, ui) => {
|
||||
|
||||
} else {
|
||||
|
||||
// code was already executed before current action
|
||||
|
||||
if(get_deferred_calls(prev) == null && get_deferred_calls(next) != null) {
|
||||
ui.calltree.render_deferred_calls(next)
|
||||
}
|
||||
@@ -256,6 +262,16 @@ export const apply_side_effects = (prev, next, ui) => {
|
||||
}
|
||||
|
||||
ui.logs.render_logs(next, prev.logs, next.logs)
|
||||
|
||||
|
||||
// Redraw canvas
|
||||
if(
|
||||
prev.current_calltree_node != next.current_calltree_node
|
||||
||
|
||||
prev.calltree_node_is_expanded != next.calltree_node_is_expanded
|
||||
) {
|
||||
redraw_canvas(next, ui.is_focus_in_editor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -501,6 +501,8 @@ export const eval_modules = (
|
||||
window: globalThis.app_window,
|
||||
|
||||
storage,
|
||||
|
||||
canvas_ops: {ops: [], contexts: new Set()},
|
||||
}
|
||||
|
||||
const result = run(module_fns, cxt, io_trace)
|
||||
|
||||
@@ -31,6 +31,22 @@ export const examples = [
|
||||
path: 'plot',
|
||||
entrypoint: 'plot/index.js',
|
||||
},
|
||||
|
||||
{
|
||||
path: 'fractal_tree',
|
||||
entrypoint: 'fractal_tree/fractal_tree.js',
|
||||
with_app_window: true,
|
||||
},
|
||||
{
|
||||
path: 'animated_fractal_tree',
|
||||
entrypoint: 'animated_fractal_tree/animated_fractal_tree.js',
|
||||
with_app_window: true,
|
||||
},
|
||||
{
|
||||
path: 'canvas_animation_bubbles',
|
||||
entrypoint: 'canvas_animation_bubbles/bubbles.js',
|
||||
with_app_window: true,
|
||||
},
|
||||
].map(e => ({...e, entrypoint: e.entrypoint ?? e.path}))
|
||||
|
||||
const files_list = examples
|
||||
|
||||
@@ -309,8 +309,9 @@ export const exec = (cmd, ...args) => {
|
||||
|
||||
// Wrap with_code_execution, because rendering values can trigger execution
|
||||
// of code by toString() and toJSON() methods
|
||||
|
||||
with_code_execution(() => {
|
||||
apply_side_effects(state, nextstate, ui)
|
||||
apply_side_effects(state, nextstate, ui, cmd)
|
||||
|
||||
if(effects != null) {
|
||||
(Array.isArray(effects) ? effects : [effects]).forEach(e => {
|
||||
|
||||
@@ -19,33 +19,61 @@ const io_patch = (window, path, use_context = false) => {
|
||||
obj[method].__original = original
|
||||
}
|
||||
|
||||
export const abort_replay = (cxt) => {
|
||||
cxt.io_trace_is_replay_aborted = true
|
||||
cxt.io_trace_abort_replay()
|
||||
// throw error to prevent further code execution. It
|
||||
// is not necesseary, because execution would not have
|
||||
// any effects anyway
|
||||
const error = new Error('io replay aborted')
|
||||
error.__ignore = true
|
||||
throw error
|
||||
}
|
||||
|
||||
const patched_method_prolog = (window, original, that, has_new_target, args) => {
|
||||
const cxt = window.__cxt
|
||||
|
||||
if(cxt.io_trace_is_replay_aborted) {
|
||||
// Try to finish fast
|
||||
const error = new Error('io replay was aborted')
|
||||
error.__ignore = true
|
||||
throw error
|
||||
}
|
||||
|
||||
// save call, so on expand_call and find_call IO functions would not be
|
||||
// called.
|
||||
// TODO: we have a problem when IO function is called from third-party
|
||||
// lib and async context is lost
|
||||
set_record_call(cxt)
|
||||
|
||||
if(cxt.is_recording_deferred_calls) {
|
||||
// TODO record trace on deferred calls?
|
||||
const value = has_new_target
|
||||
? new original(...args)
|
||||
: original.apply(that, args)
|
||||
return {is_return: true, value}
|
||||
}
|
||||
|
||||
return {is_return: false}
|
||||
}
|
||||
|
||||
const make_patched_method = (window, original, name, use_context) => {
|
||||
const method = function(...args) {
|
||||
|
||||
const cxt = window.__cxt
|
||||
|
||||
if(cxt.io_trace_is_replay_aborted) {
|
||||
// Try to finish fast
|
||||
const error = new Error('io replay was aborted')
|
||||
error.__ignore = true
|
||||
throw error
|
||||
}
|
||||
|
||||
// save call, so on expand_call and find_call IO functions would not be
|
||||
// called.
|
||||
// TODO: we have a problem when IO function is called from third-party
|
||||
// lib and async context is lost
|
||||
set_record_call(cxt)
|
||||
|
||||
const has_new_target = new.target != null
|
||||
|
||||
if(cxt.is_recording_deferred_calls) {
|
||||
// TODO record trace on deferred calls?
|
||||
return has_new_target
|
||||
? new original(...args)
|
||||
: original.apply(this, args)
|
||||
const {value, is_return} = patched_method_prolog(
|
||||
window,
|
||||
original,
|
||||
this,
|
||||
has_new_target,
|
||||
args
|
||||
)
|
||||
if(is_return) {
|
||||
return value
|
||||
}
|
||||
|
||||
const cxt = window.__cxt
|
||||
|
||||
const cxt_copy = cxt
|
||||
|
||||
if(cxt.io_trace_is_recording) {
|
||||
@@ -142,14 +170,7 @@ const make_patched_method = (window, original, name, use_context) => {
|
||||
)
|
||||
)
|
||||
){
|
||||
cxt.io_trace_is_replay_aborted = true
|
||||
cxt.io_trace_abort_replay()
|
||||
// throw error to prevent further code execution. It
|
||||
// is not necesseary, becuase execution would not have
|
||||
// any effects anyway
|
||||
const error = new Error('io replay aborted')
|
||||
error.__ignore = true
|
||||
throw error
|
||||
abort_replay(cxt)
|
||||
} else {
|
||||
|
||||
const next_resolution = cxt.io_trace.find((e, i) =>
|
||||
@@ -179,6 +200,7 @@ const make_patched_method = (window, original, name, use_context) => {
|
||||
|
||||
const next_event = cxt.io_trace[cxt.io_trace_index]
|
||||
if(next_event.type == 'call') {
|
||||
// TODO use abort_replay
|
||||
cxt.io_trace_is_replay_aborted = true
|
||||
cxt.io_trace_abort_replay()
|
||||
} else {
|
||||
@@ -267,6 +289,41 @@ const patch_Date = (window) => {
|
||||
io_patch(window, ['Date', 'now'])
|
||||
}
|
||||
|
||||
const patch_interval = (window, name) => {
|
||||
const original = window[name]
|
||||
|
||||
window[name] = function(...args) {
|
||||
const has_new_target = new.target != null
|
||||
const {value, is_return} = patched_method_prolog(
|
||||
window,
|
||||
original,
|
||||
this,
|
||||
has_new_target,
|
||||
args
|
||||
)
|
||||
if(is_return) {
|
||||
return value
|
||||
}
|
||||
|
||||
const cxt = window.__cxt
|
||||
|
||||
if(!cxt.io_trace_is_recording) {
|
||||
/*
|
||||
Discard io_trace. Run code without IO trace because it is not clear
|
||||
how it should work with io trace
|
||||
*/
|
||||
abort_replay(cxt)
|
||||
}
|
||||
|
||||
return has_new_target
|
||||
? new original(...args)
|
||||
: original.apply(this, args)
|
||||
}
|
||||
|
||||
Object.defineProperty(window[name], 'name', {value: name})
|
||||
}
|
||||
|
||||
|
||||
export const apply_io_patches = (window) => {
|
||||
io_patch(window, ['Math', 'random'])
|
||||
|
||||
@@ -276,7 +333,8 @@ export const apply_io_patches = (window) => {
|
||||
// replaying from trace
|
||||
io_patch(window, ['clearTimeout'])
|
||||
|
||||
// TODO patch setInterval to only cleanup all intervals on finish
|
||||
patch_interval(window, 'setInterval')
|
||||
patch_interval(window, 'clearInterval')
|
||||
|
||||
patch_Date(window)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {defineMultiversionArray, create_array, wrap_array} from './array.js'
|
||||
import {create_object} from './object.js'
|
||||
import {defineMultiversionSet} from './set.js'
|
||||
import {defineMultiversionMap} from './map.js'
|
||||
import {apply_canvas_patches} from '../canvas.js'
|
||||
|
||||
/*
|
||||
Converts generator-returning function to promise-returning function. Allows to
|
||||
@@ -44,6 +45,7 @@ export const run = gen_to_promise(function*(module_fns, cxt, io_trace) {
|
||||
defineMultiversion(cxt.window)
|
||||
apply_io_patches(cxt.window)
|
||||
inject_leporello_api(cxt)
|
||||
apply_canvas_patches(cxt.window)
|
||||
cxt.window.__is_initialized = true
|
||||
} else {
|
||||
throw new Error('illegal state')
|
||||
|
||||
Reference in New Issue
Block a user