TabMob */ public array $TabMob = []; /** @var array, map:string}> TabPalette */ public array $TabPalette = []; public Port $Ecran1; public Port $Ecran2; public Port $debecr; public Port $BuffMouse; public Port $mainWindow; /** the current port (AcamSetPortOffscreen / CarboniseSetPortWindow) */ public Port $curPort; // QuickDraw state of the current drawing (one for all the ports: the game always sets it) public int $qdFore = Mac::BLACK; public int $qdBack = Mac::WHITE; public int $qdPenW = 1; public int $qdPenH = 1; public int $qdPenX = 0; public int $qdPenY = 0; /** 'black' | 'gray' (CarboniseSetPatGray / Black) */ public string $qdPat = 'black'; /** PenMode(patOr): only the set pixels of the pattern are drawn */ public bool $qdPatOr = false; /** @var array{0:int,1:int,2:int,3:int}|null ClipRect of the current drawing (text of the message) */ public ?array $qdClip = null; public static function SetRect(int $l, int $t, int $r, int $b): array { return [$l, $t, $r, $b]; } public static function OffsetRect(array $r, int $dh, int $dv): array { return [$r[0] + $dh, $r[1] + $dv, $r[2] + $dh, $r[3] + $dv]; } public static function InsetRect(array $r, int $dh, int $dv): array { return [$r[0] + $dh, $r[1] + $dv, $r[2] - $dh, $r[3] - $dv]; } public static function EmptyRect(array $r): bool { return $r[2] <= $r[0] || $r[3] <= $r[1]; } public static function PtInRect(array $pt, array $r): bool { return $pt[0] >= $r[0] && $pt[0] < $r[2] && $pt[1] >= $r[1] && $pt[1] < $r[3]; } /** @return array|null the intersection, null if empty */ public static function SectRect(array $a, array $b): ?array { $r = [max($a[0], $b[0]), max($a[1], $b[1]), min($a[2], $b[2]), min($a[3], $b[3])]; return self::EmptyRect($r) ? null : $r; } public function AcamSetPortOffscreen(Port $p): void { $this->curPort = $p; } public function AcamGetPortOffscreen(): Port { return $this->curPort; } public function CarboniseSetPortWindow(): void { $this->curPort = $this->mainWindow; } public function initPorts(): void { Mac::init(); $this->Ecran1 = new Port(256 * 2, 100 * 2); $this->Ecran2 = new Port(256 * 2, 100 * 2); $this->debecr = new Port(320 * 2, 200 * 2); $this->BuffMouse = new Port(64 * 2, 64 * 2); $this->mainWindow = new Port(320 * 2, 200 * 2); $this->curPort = $this->mainWindow; } // ---- mobs -------------------------------------------------------------------------------------- /** * ChargeFichierBrt: palette, then the sprites (big-endian sizes, raw indexes). * @return int number of mobs loaded */ public function ChargeFichierBrt(string $file, int $id, int $nbMobs, int $vide): int { $b = file_get_contents($file); if ($b === false || substr($b, 0, 4) !== 'BRUT') { throw new \RuntimeException("ChargeFichierBrt: bad file $file"); } $n = ord($b[4]) + 1; $rgb = []; $map = ''; for ($i = 0; $i < $n; $i++) { $r = ord($b[5 + $i * 3]); $g = ord($b[6 + $i * 3]); $bl = ord($b[7 + $i * 3]); $rgb[$i] = [($r << 8) | $r, ($g << 8) | $g, ($bl << 8) | $bl]; $map .= chr(Mac::color2Index($rgb[$i][0], $rgb[$i][1], $rgb[$i][2])); } $map = str_pad($map, 256, "\0"); $palette = count($this->TabPalette); $this->TabPalette[$palette] = ['n' => $n, 'rgb' => $rgb, 'map' => $map]; $p = 5 + $n * 3; $count = unpack('n', $b, $p)[1]; if ($count !== $nbMobs) { throw new \RuntimeException("ChargeFichierBrt: $file has $count mobs, $nbMobs expected"); } $p += 2; for ($i = 0; $i < $count; $i++, $id++) { [, $w, $h] = unpack('n2', $b, $p); $p += 4; $mob = new Mob($w, $h, $palette, $vide, substr($b, $p, $w * $h)); $mob->prepare($map); $this->TabMob[$id] = $mob; $p += $w * $h; } return $count; } /** * CalculePaletteAttenuee: the palette of a mob, the $nbCoul first colours multiplied by $coeff. * @return string a byte map (tabIndex) */ public function CalculePaletteAttenuee(int $num, float $coeff, int $nbCoul): string { $coeffl = (int)(256.0 * $coeff); $palette = $this->TabPalette[$this->TabMob[$num]->indexPalette]; $map = ''; for ($i = 0; $i < $palette['n']; $i++) { $c = $palette['rgb'][$i]; if ($i < $nbCoul) { $map .= chr(Mac::color2Index( min(65535, ($coeffl * $c[0]) >> 8), min(65535, ($coeffl * $c[1]) >> 8), min(65535, ($coeffl * $c[2]) >> 8) )); } else { $map .= chr(Mac::color2Index($c[0], $c[1], $c[2])); } } return str_pad($map, 256, "\0"); } /** The default map of the palette of a mob */ public function mobMap(int $num): string { return $this->TabPalette[$this->TabMob[$num]->indexPalette]['map']; } public function CalculeAireMob(int $num, int $x, int $y): array { $mob = $this->TabMob[$num]; $x *= 2; $y *= 2; return [$x, $y, $x + $mob->nbcol, $y + $mob->nbligne]; } /** * Affmob3d: a mob scaled into the rectangle $aire of the current port (pixels), with the palette * map $tabIndex (null = the palette of the mob). */ public function Affmob3d(int $num, array $aire, ?string $tabIndex = null): void { $mob = $this->TabMob[$num] ?? null; if ($mob === null || $mob->nbcol <= 0 || $mob->nbligne <= 0) { return; } $port = $this->curPort; $tx = $mob->nbcol; $ty = $mob->nbligne; [$l, $t, $r, $b] = $aire; $i = $r - $l; $j = $b - $t; if ($i <= 0 || $j <= 0) { return; } $pasX = intdiv($tx << 16, $i); $pasY = intdiv($ty << 16, $j); $x0 = 0; $y0 = 0; if ($l < 0) { $x0 = (int)(-$l * $pasX / 65536.0); $l = 0; } if ($t < 0) { $y0 = (int)(-$t * $pasY / 65536.0); $t = 0; } $r = min($r, $port->w); $b = min($b, $port->h); if ($r <= $l || $b <= $t) { return; } $w = $r - $l; // columns of the source for each column of the destination $cols = []; $sx = $x0; $rest = 0; for ($k = 0; $k < $w; $k++) { $cols[$k] = $sx; $rest += $pasX; $sx += $rest >> 16; $rest &= 0xFFFF; } if ($sx > $tx) { foreach ($cols as $k => $c) { if ($c >= $tx) { $cols[$k] = $tx - 1; } } } $identity = $w === $tx && $x0 === 0; $cache = []; $sy = $y0; $restY = 0; for ($y = $t; $y < $b; $y++) { if ($sy >= $ty) { break; } if (!isset($cache[$sy])) { $src = $tabIndex === null ? $mob->rows[$sy] : $mob->rowWith($sy, $tabIndex); $mask = $mob->masks[$sy]; if ($identity) { $cache[$sy] = [$src, $mask]; } else { $s = ''; $m = ''; foreach ($cols as $c) { $s .= $src[$c]; $m .= $mask[$c]; } $cache[$sy] = [$s, $m]; } } [$s, $m] = $cache[$sy]; $row = $port->rows[$y]; $port->rows[$y] = substr_replace($row, (substr($row, $l, $w) & $m) | $s, $l, $w); $restY += $pasY; $sy += $restY >> 16; $restY &= 0xFFFF; } $port->dirty = true; } /** * Affmobbrt: a mob at ($x, $y) in ST coordinates (0-320, 0-200) of the current port, flipped if * $sens, with the palette map $tabIndex (null = the palette of the mob). */ public function Affmobbrt(int $num, int $x, int $y, int $sens, ?string $tabIndex = null): void { $mob = $this->TabMob[$num] ?? null; if ($mob === null || $mob->nbcol <= 0 || $mob->nbligne <= 0) { return; } $x *= 2; $y *= 2; // a flipped mob is drawn from x leftwards: its left edge is x - nbcol + 1 $left = $sens ? $x - $mob->nbcol + 1 : $x; $this->blitRows($this->curPort, $mob, $left, $y, (bool)$sens, $tabIndex); } /** Draws the rows of a mob at the pixel ($x, $y), clipped. */ private function blitRows(Port $port, Mob $mob, int $x, int $y, bool $flip, ?string $tabIndex, ?string $color = null): void { $tx = $mob->nbcol; $ty = $mob->nbligne; $sx = 0; $w = $tx; if ($x < 0) { $sx = -$x; $w += $x; $x = 0; } if ($x + $w > $port->w) { $w = $port->w - $x; } if ($w <= 0) { return; } $y0 = max(0, -$y); $y1 = min($ty, $port->h - $y); for ($j = $y0; $j < $y1; $j++) { if ($color !== null) { $mask = $mob->masks[$j]; $src = ~$mask & str_repeat($color, $tx); } else { $src = $tabIndex === null ? $mob->rows[$j] : $mob->rowWith($j, $tabIndex); $mask = $mob->masks[$j]; } if ($flip) { $src = strrev($src); $mask = strrev($mask); } if ($sx || $w !== $tx) { $src = substr($src, $sx, $w); $mask = substr($mask, $sx, $w); } $row = $port->rows[$y + $j]; $port->rows[$y + $j] = substr_replace($row, (substr($row, $x, $w) & $mask) | $src, $x, $w); } $port->dirty = true; } /** AffmobCar: a mob in one colour at the pixel ($x, $y) of the current port (the font glyphs). */ public function AffmobCar(int $num, int $x, int $y, int $couleur): void { $mob = $this->TabMob[$num] ?? null; if ($mob === null || $mob->nbcol <= 0 || $mob->nbligne <= 0) { return; } if ($this->qdClip !== null) { $this->blitClipped($mob, $x, $y, $couleur); return; } $this->blitRows($this->curPort, $mob, $x, $y, false, null, chr($couleur)); } private function blitClipped(Mob $mob, int $x, int $y, int $couleur): void { [$cl, $ct, $cr, $cb] = $this->qdClip; $port = $this->curPort; for ($j = 0; $j < $mob->nbligne; $j++) { $py = $y + $j; if ($py < $ct || $py >= $cb || $py < 0 || $py >= $port->h) { continue; } $mask = $mob->masks[$j]; for ($i = 0; $i < $mob->nbcol; $i++) { $px = $x + $i; if ($mask[$i] === "\0" && $px >= $cl && $px < $cr && $px >= 0 && $px < $port->w) { $port->rows[$py][$px] = chr($couleur); } } } $port->dirty = true; } /** * Affmobbrtomb: a mob at ($x, $y) (ST coordinates) with its shadow ($deltah, $deltav ST pixels * further): the screen under the shadow is darkened (TabCvtCoulOmbre). * @return array the rectangle of the mob and its shadow */ public function Affmobbrtomb(int $num, int $x, int $y, int $deltah, int $deltav): array { $mob = $this->TabMob[$num] ?? null; if ($mob === null) { return [0, 0, 0, 0]; } $tx = $mob->nbcol; $ty = $mob->nbligne; $deltah *= 2; $deltav *= 2; $x *= 2; $y *= 2; $r = [$x, $y, $x + $tx + $deltah, $y + $ty + $deltav]; if ($tx <= 0 || $ty <= 0) { return $r; } $port = $this->curPort; // the shadow $sx = $x + $deltah; $l = max(0, $sx); $rr = min($port->w, $sx + $tx); if ($rr > $l) { for ($j = 0; $j < $ty; $j++) { $py = $y + $j + $deltav; if ($py < 0 || $py >= $port->h || $py + 1 >= 400) { continue; } $mask = substr($mob->masks[$j], $l - $sx, $rr - $l); $seg = substr($port->rows[$py], $l, $rr - $l); $dark = strtr($seg, Mac::IDENT, Mac::$shadow); $port->rows[$py] = substr_replace($port->rows[$py], ($seg & $mask) | ($dark & ~$mask), $l, $rr - $l); } } $this->blitRows($port, $mob, $x, $y, false, null); return $r; } public function PlotIndexPixBrt(Port $port, int $x, int $y, int $coul): void { $port->plot($x, $y, $coul); } public function GetIndexPixBrt(Port $port, int $x, int $y): int { return $port->get($x, $y); } /** SwapCouleur: exchanges two colours in a rectangle of a port */ public function SwapCouleur(Port $port, array $r, int $coul1, int $coul2): void { $map = Mac::IDENT; $map[$coul1] = chr($coul2); $map[$coul2] = chr($coul1); $port->mapRect($r[0], $r[1], $r[2], $r[3], $map); } /** BoxFill: rectangle (pixels) of a port in an ST colour */ public function BoxFill(Port $port, array $r, int $couleur): void { $port->fill($r[0], $r[1], $r[2], $r[3], Mac::st($couleur)); } /** RemplitLigneBrut: ST lines y1..y2 of the current port with a system index */ public function RemplitLigneBrut(int $y1, int $y2, int $index): void { $this->curPort->fill(0, $y1 * 2, $this->curPort->w, $y2 * 2, $index); } /** * RemplitZoneLignes: from the pixel line $y, $taille lines of the current port, one colour each. * @param int[] $tabCouleur system indexes */ public function RemplitZoneLignes(int $y, int $taille, array $tabCouleur, int $offset = 0): void { $port = $this->curPort; for ($i = 0; $i < $taille; $i++) { if ($y + $i >= 0 && $y + $i < $port->h) { $port->rows[$y + $i] = str_repeat(chr($tabCouleur[$offset + $i]), $port->w); } } $port->dirty = true; } /** DebDeg: dither patterns of the disks, 32 bits each (as strings: the browser has 32-bit ints) */ private const DEB_DEG = [ '10000000000000000000000000000000', '11000000000000000000000000000000', '10100000000000000000000000000000', '11010000000000000000000000000000', '11001000000000000000000000000000', '11011000000000000000000000000000', '10101100000000000000000000000000', '11010100000000000000000000000000', '11110101000000000000000000000000', '11101100100000000000000000000000', '11101100100000000000000000000000', '11110110100000000000000000000000', '11110101011010000000000000000000', '11111101110100000000000000000000', '11111010101101000000000000000000', '11111000110100000000000000000000', '11111101101111001000000000000000', '11111110111011001000000000000000', '11111011101010010000000000000000', '11101110111010100000000000000000', '11110110101100110000000000000000', '11111101101010110100000000000000', '11110110101101011010000000000000', '11111101110110010100101000000000', '11111011011001010010010000000000', '11111101111001100001100000000000', '11110111011110011011001000000000', '11111101101011110010101010000000', '11111111011101011110110001101000', '11111111011111010110110110010100', '11111101110101101011011011010000', '11111101110010110100101000010100', '11110110111011000110010101100000', '11101111101110101001001100000000', '11110111011101011011010101000000', ]; private const TABPOURC = [3276, 9830, 19660, 32767, 45874, 55704, 62258, 65535]; /** * TraceDisque: a disk of the trees of the side view (8 colours of the palette palhaute/tabcoul, * dithered), from the edges ($dg, $dd at line $dy) to ($fg, $fd), $epaisseur lines up. * $dg, $dd, $dy are updated (by reference) like the C pointers. */ public function TraceDisque(int &$dg, int &$dd, int &$dy, int $fg, int $fd, int $epaisseur): void { $fy = 0; if ($dy > 0 && $epaisseur > 0) { $port = $this->curPort; $fy = $dy * 2; $epaisseur *= 2; $x1 = ($dg << 16) * 2; $x2 = ($dd << 16) * 2; $dx1 = intdiv(2 * (($fg - $dg) << 16), $epaisseur); $dx2 = intdiv(2 * (($fd - $dd) << 16), $epaisseur); $index = []; for ($i = 0; $i < 9; $i++) { $index[$i] = chr(Mac::st($this->palhaute[$this->tabcoul[$i]] ?? 0)); } while ($fy && $epaisseur--) { if ($fy < 200) { $delta = ($x2 - $x1) >> 16; $sx1 = $x1 >> 16; for ($i = 0; $i < 8; $i++) { $sx2 = ($x1 >> 16) + (($delta * self::TABPOURC[$i]) >> 16); if ($sx1 < 0) { $sx1 = 0; } if ($sx2 > 511) { $sx2 = 511; } if ($sx1 < 512 && $sx2 > 0 && $sx1 !== $sx2 && $sx2 > $sx1 && $fy >= 0 && $fy < $port->h) { $tsx = $sx2 - $sx1; $bits = self::DEB_DEG[($tsx > 32 ? 31 : $tsx - 1) + ($fy & 3)]; $seg = strtr(str_pad(substr($bits, 0, min($tsx, 32)), $tsx, '0'), ['1' => $index[$i], '0' => $index[$i + 1]]); $port->rows[$fy] = substr_replace($port->rows[$fy], $seg, $sx1, $tsx); } $sx1 = $sx2; } } $x1 += $dx1; $x2 += $dx2; $fy--; } $port->dirty = true; } $dg = $fg; $dd = $fd; if ($fy <= 0) { $dy = 0; } else { $dy = intdiv($fy, 2); } } /** CopyOffscreen: rectangle $rs of $source to the rectangle $rd of $dest (same size). */ public function CopyOffscreen(Port $source, Port $dest, array $rs, array $rd): void { if (self::EmptyRect($rs)) { return; } $dest->copyFrom($source, $rs[0], $rs[1], $rs[2], $rs[3], $rd[0], $rd[1]); } public function effdemi(): void { $r = [0, 32 * 2, 255 * 2, (32 + 66) * 2]; $this->CopyOffscreen($this->Ecran2, $this->Ecran1, $r, $r); } public function tfrdemi(): void { $this->CopyOffscreen($this->Ecran1, $this->mainWindow, [0, 32 * 2, 255 * 2, (32 + 66) * 2], [32 * 2, 33 * 2, (32 + 255) * 2, (33 + 66) * 2]); } public function effima(int $couleur): void { $this->BoxFill($this->Ecran1, [0, 0, 255 * 2, 100 * 2], $couleur); } public function effvirt(int $couleur): void { $this->BoxFill($this->debecr, [32 * 2, 1 * 2, (32 + 255) * 2, 101 * 2], $couleur); } public function tfrima(): void { $this->CopyOffscreen($this->Ecran1, $this->mainWindow, [0, 0, 255 * 2, 100 * 2], [32 * 2, 1 * 2, (32 + 255) * 2, (1 + 100) * 2]); } public function tfrvirt(): void { $this->CopyOffscreen($this->Ecran1, $this->debecr, [0, 0, 255 * 2, 100 * 2], [32 * 2, 1 * 2, (32 + 255) * 2, (1 + 100) * 2]); } /** tfrmenufenetre: the bottom of the window active (copied from debecr) or greyed */ public function tfrmenufenetre(int $etat): void { $r = [0, 113 * 2, 320 * 2, 200 * 2]; if ($etat) { $this->CopyOffscreen($this->debecr, $this->mainWindow, $r, $r); } else { $gbak = $this->curPort; $this->CarboniseSetPortWindow(); $this->qdPat = 'gray'; $this->RGBForeColor(15000, 6000, 2000); $this->qdPatOr = true; $this->DoPaintRect($r); $this->qdFore = Mac::BLACK; $this->qdPat = 'black'; $this->qdPatOr = false; $this->curPort = $gbak; } } public function CopyEcran1Sur2(): void { $r = [0, 0, 255 * 2, 100 * 2]; $this->CopyOffscreen($this->Ecran1, $this->Ecran2, $r, $r); } /** TfrEcr: a rectangle of debecr (null = all) to the window */ public function TfrEcr(?array $r = null): void { $r ??= [0, 0, 640, 400]; $this->CopyOffscreen($this->debecr, $this->mainWindow, $r, $r); } public function Affmob(int $mob, int $y, int $x, int $inverse): void { $gbak = $this->curPort; $this->curPort = $this->Ecran1; $this->Affmobbrt($mob, $x, $y, $inverse); $this->curPort = $gbak; } public function AffmobConst(int $mob, int $y, int $x, int $inverse): void { $gbak = $this->curPort; $this->curPort = $this->Ecran1; $this->Affmobbrt($mob, $x, $y, $inverse); $this->curPort = $this->Ecran2; $this->Affmobbrt($mob, $x, $y, $inverse); $this->curPort = $gbak; } public function AffDebecr(int $mob, int $y, int $x, int $transfere): void { $gbak = $this->curPort; $this->curPort = $this->debecr; $this->Affmobbrt($mob, $x, $y, 0); if ($transfere) { $this->TfrEcr($this->CalculeAireMob($mob, $x, $y)); } $this->curPort = $gbak; } /** affomb: a mob and its shadow on debecr, copied to the window if $recopie */ public function affomb(int $mob, int $ligne, int $colonne, int $deltav, int $deltah, bool $recopie): void { $gbak = $this->curPort; $this->curPort = $this->debecr; $r = $this->Affmobbrtomb($mob, $colonne, $ligne, $deltah, $deltav); if ($recopie) { $this->CopyOffscreen($this->debecr, $this->mainWindow, $r, $r); } $this->curPort = $gbak; } public function AffmobPalette(int $mob, int $y, int $x, int $inverse, ?string $tabIndex): void { $gbak = $this->curPort; $this->curPort = $this->Ecran1; $this->Affmobbrt($mob, $x, $y, $inverse, $tabIndex); $this->curPort = $gbak; } /** AffichePictDansAire: help picture image_ in the rectangle of the current port */ public function AffichePictDansAire(int $num, array $r): void { $file = $this->dataDir . '/image_' . ($num + 1000 * $this->langue) . '.bmp'; if (!is_file($file)) { $file = $this->dataDir . '/image_' . ($num + 1000) . '.bmp'; } if (!is_file($file)) { return; } $this->drawBmp($file, $r[0], $r[1]); } /** Draws an 8-bit BMP of the Mac palette (converted pictures) at ($x, $y) of the current port. */ public function drawBmp(string $file, int $x, int $y, bool $remap = false): void { [$w, $h, $pal, $pix] = Bmp::decode((string)file_get_contents($file)); if ($remap) { $map = ''; for ($i = 0; $i < 256; $i++) { $c = $pal[$i] ?? [0, 0, 0]; $map .= chr(Mac::color2Index($c[0] * 257, $c[1] * 257, $c[2] * 257)); } $pix = strtr($pix, Mac::IDENT, $map); } $src = new Port($w, $h); $src->rows = str_split($pix, $w); $this->curPort->copyFrom($src, 0, 0, $w, $h, $x, $y); } // ---- QuickDraw ------------------------------------------------------------------------------- public function RGBForeColor(int $r, int $g, int $b): void { $this->qdFore = Mac::color2Index($r, $g, $b); } public function ForeColorBlack(): void { $this->qdFore = Mac::BLACK; } public function ForeColorWhite(): void { $this->qdFore = Mac::WHITE; } public function DoPenSize(int $x, int $y): void { $this->qdPenW = $x; $this->qdPenH = $y; } public function PenNormal(): void { $this->qdPenW = $this->qdPenH = 1; $this->qdPat = 'black'; $this->qdPatOr = false; } /** PaintRect with the pen colour and pattern */ public function DoPaintRect(array $r): void { $this->paint($r, $this->qdFore); } /** EraseRect: background colour (white) */ public function DoEraseRect(array $r): void { $this->curPort->fill($r[0], $r[1], $r[2], $r[3], $this->qdBack); } public function DoFrameRect(array $r): void { [$l, $t, $rr, $b] = $r; $pw = $this->qdPenW; $ph = $this->qdPenH; $this->paint([$l, $t, $rr, $t + $ph], $this->qdFore); $this->paint([$l, $b - $ph, $rr, $b], $this->qdFore); $this->paint([$l, $t, $l + $pw, $b], $this->qdFore); $this->paint([$rr - $pw, $t, $rr, $b], $this->qdFore); } private function paint(array $r, int $color): void { $port = $this->curPort; if ($this->qdPat === 'gray') { // 50% pattern; with patOr only its set pixels are drawn, else the others get the background $l = max(0, $r[0]); $rr = min($port->w, $r[2]); for ($y = max(0, $r[1]); $y < min($port->h, $r[3]); $y++) { for ($x = $l; $x < $rr; $x++) { if ((($x + $y) & 1) === 0) { $port->rows[$y][$x] = chr($color); } elseif (!$this->qdPatOr) { $port->rows[$y][$x] = chr($this->qdBack); } } } $port->dirty = true; return; } $port->fill($r[0], $r[1], $r[2], $r[3], $color); } public function DoMoveTo(int $x, int $y): void { $this->qdPenX = $x; $this->qdPenY = $y; } public function DoLineTo(int $x, int $y): void { $this->drawLine($this->qdPenX, $this->qdPenY, $x, $y); $this->qdPenX = $x; $this->qdPenY = $y; } public function DoLine(int $dx, int $dy): void { $this->DoLineTo($this->qdPenX + $dx, $this->qdPenY + $dy); } /** A line with the pen (QuickDraw: the pen hangs below and right of the path) */ private function drawLine(int $x0, int $y0, int $x1, int $y1): void { $dx = abs($x1 - $x0); $dy = -abs($y1 - $y0); $sx = $x0 < $x1 ? 1 : -1; $sy = $y0 < $y1 ? 1 : -1; $err = $dx + $dy; $port = $this->curPort; $c = chr($this->qdFore); $pw = $this->qdPenW; $ph = $this->qdPenH; while (true) { for ($j = 0; $j < $ph; $j++) { $yy = $y0 + $j; if ($yy < 0 || $yy >= $port->h) { continue; } for ($i = 0; $i < $pw; $i++) { $xx = $x0 + $i; if ($xx >= 0 && $xx < $port->w) { $port->rows[$yy][$xx] = $c; } } } if ($x0 === $x1 && $y0 === $y1) { break; } $e2 = 2 * $err; if ($e2 >= $dy) { $err += $dy; $x0 += $sx; } if ($e2 <= $dx) { $err += $dx; $y0 += $sy; } } $port->dirty = true; } /** ScrollRect: moves the content of a rectangle of the current port */ public function DoScrollRect(array $r, int $dh, int $dv): void { $port = $this->curPort; $copy = new Port($r[2] - $r[0], $r[3] - $r[1]); $copy->copyFrom($port, $r[0], $r[1], $r[2], $r[3], 0, 0); $clip = $this->qdClip; $port->fill($r[0], $r[1], $r[2], $r[3], $this->qdBack); $dest = new Port($port->w, $port->h); $dest->rows = $port->rows; $dest->copyFrom($copy, 0, 0, $copy->w, $copy->h, $r[0] + $dh, $r[1] + $dv); // only inside the rectangle for ($y = max(0, $r[1]); $y < min($port->h, $r[3]); $y++) { $port->rows[$y] = substr_replace($port->rows[$y], substr($dest->rows[$y], $r[0], $r[2] - $r[0]), $r[0], $r[2] - $r[0]); } $this->qdClip = $clip; $port->dirty = true; } }